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, 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        helpers as rpc_helpers,
112        http::{BlockchainHttpRpcClient, EXECUTION_RPC_TIMEOUT_SECS},
113        types::{RpcCallType, RpcTransaction, RpcTransactionReceipt},
114        verification::{
115            VerificationCoordinator, VerificationOutcome, Verified, VerifiedBlockHeader,
116            VerifiedCallTrace, VerifiedSimulation,
117        },
118    },
119};
120
121/// Interval between receipt polls while awaiting transaction finality.
122const RECEIPT_POLL_INTERVAL: Duration = Duration::from_secs(1);
123const MAX_PAYLOAD_OPERATION_BATCH_SIZE: usize = 1_000;
124/// Basis points denominator for slippage derivation.
125const BPS_DENOMINATOR: u32 = 10_000;
126/// Denial reason for order-list submissions, which have no on-chain execution route.
127const ORDER_LIST_UNSUPPORTED: &str =
128    "Order lists are not supported; submit each order individually";
129/// Rejection reason for order modifications, which immutable on-chain swaps cannot support.
130const ORDER_MODIFY_UNSUPPORTED: &str = "Order modification is not supported";
131/// Rejection reason for order cancellations, which immutable on-chain swaps cannot support.
132const ORDER_CANCEL_UNSUPPORTED: &str = "Order cancellation is not supported";
133/// Error for venue report probes that cannot answer without implying absence.
134const VENUE_EXECUTION_REPORTS_UNSUPPORTED: &str =
135    "Venue execution reports are not supported on the blockchain execution client";
136/// Maximum historical block range inspected to identify a signer-nonce replacement.
137const MAX_REPLACEMENT_SCAN_BLOCKS: u64 = 4_096;
138
139/// Result of authenticating every persisted signed transaction in one execution database.
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub struct PayloadStorageCheck {
142    /// Whether payload protection is active.
143    pub protected: bool,
144    /// Durable deployment identity when protection is active.
145    pub deployment_id: Option<String>,
146    /// Rows which still contain plaintext signed transaction bytes.
147    pub plaintext_rows: u64,
148    /// Signed transaction rows which require a payload.
149    pub original_rows: u64,
150    /// Canonical replacement rows whose original bytes are unavailable.
151    pub replacement_rows: u64,
152    /// Payload rows successfully opened and authenticated.
153    pub authenticated_rows: u64,
154    /// Key IDs referenced by protected payloads.
155    pub key_ids: Vec<String>,
156    /// Database roles with direct table ownership or `SELECT` grants.
157    pub read_roles: Vec<String>,
158}
159
160impl From<ExecutionPayloadCheck> for PayloadStorageCheck {
161    fn from(value: ExecutionPayloadCheck) -> Self {
162        Self {
163            protected: value.protected,
164            deployment_id: value.deployment_id,
165            plaintext_rows: value.plaintext_rows,
166            original_rows: value.original_rows,
167            replacement_rows: value.replacement_rows,
168            authenticated_rows: value.authenticated_rows,
169            key_ids: value.key_ids,
170            read_roles: value.read_roles,
171        }
172    }
173}
174
175// A broadcast transaction awaiting finality, occupying the single in-flight slot.
176#[derive(Debug, Clone, Copy)]
177struct InFlightTransaction {
178    intent_id: i64,
179    nonce: u64,
180    tx_hash: B256,
181    purpose: TransactionPurpose,
182}
183
184#[derive(Debug, Clone, Copy)]
185struct RecoveryTransaction {
186    intent_id: i64,
187    nonce: u64,
188    purpose: TransactionPurpose,
189}
190
191/// The single in-flight transaction slot.
192///
193/// The slot is claimed before any preparation RPC call so the `pending` nonce read stays
194/// authoritative: a second transaction is rejected before it can sign. A claim is released
195/// only when preparation fails before signing; from persistence onward the slot is never
196/// released on failure, because the database may have committed before its acknowledgement
197/// was lost.
198#[derive(Debug, Clone, Copy)]
199enum InFlightSlot {
200    /// Claimed before preparation; no signed transaction exists yet.
201    Preparing(TransactionPurpose),
202    /// Restored durable ownership retained while persisted transaction data is authenticated.
203    Recovering(RecoveryTransaction),
204    /// Signed, persisted, and awaiting finality.
205    AwaitingFinality(InFlightTransaction),
206}
207
208#[derive(Debug, Clone)]
209struct IncludedTransaction {
210    intent_id: i64,
211    nonce: u64,
212    tx_hash: B256,
213    block_number: u64,
214    receipt: RpcTransactionReceipt,
215    finality: StableFinality,
216}
217
218#[derive(Debug, Clone)]
219struct StableFinality {
220    decisions: Vec<ExecutionVerificationDecision>,
221    inclusion_header: ExecutionVerifiedHeader,
222    finalized_headers: Vec<ExecutionVerifiedHeader>,
223}
224
225#[derive(Debug, Clone, Copy)]
226enum TransactionAuthorization {
227    Wrap {
228        weth: Address,
229    },
230    Approve {
231        token: Address,
232        router: Address,
233        amount: U256,
234    },
235}
236
237/// The single-in-flight limit error naming the transaction currently occupying the slot.
238fn in_flight_limit_error(slot: &InFlightSlot) -> anyhow::Error {
239    match slot {
240        InFlightSlot::Preparing(purpose) => anyhow::anyhow!(
241            "A {} transaction is being prepared; at most one transaction can be in flight",
242            purpose.as_str()
243        ),
244        InFlightSlot::Recovering(recovery) => anyhow::anyhow!(
245            "Execution intent {} ({}, nonce {}) retains signer ownership pending recovery; at most one transaction can be in flight",
246            recovery.intent_id,
247            recovery.purpose.as_str(),
248            recovery.nonce
249        ),
250        InFlightSlot::AwaitingFinality(in_flight) => anyhow::anyhow!(
251            "Transaction {} (intent {}, {}, nonce {}) is still awaiting finality; at most one transaction can be in flight",
252            in_flight.tx_hash,
253            in_flight.intent_id,
254            in_flight.purpose.as_str(),
255            in_flight.nonce
256        ),
257    }
258}
259
260/// Releases a pre-signature slot claim when the slot is still in the preparing state.
261///
262/// Aborted or failed preparation can leave a claim behind; because no signed transaction
263/// exists for a preparing slot, releasing it cannot strand a broadcastable signature.
264fn release_preparing_slot(in_flight: &Mutex<Option<InFlightSlot>>) {
265    let mut slot = in_flight.lock();
266    if matches!(*slot, Some(InFlightSlot::Preparing(_))) {
267        *slot = None;
268    }
269}
270
271fn release_preparing_if_reservation_not_committed(
272    in_flight: &Mutex<Option<InFlightSlot>>,
273    error: &anyhow::Error,
274) {
275    if reservation_failure_proven_not_committed(error) {
276        release_preparing_slot(in_flight);
277    }
278}
279
280#[derive(Debug)]
281struct TransactionLimits {
282    allowed_token_pairs: HashSet<(Address, Address)>,
283    quote_spend_limits: HashMap<(Address, Address), QuoteSpendCeiling>,
284    slippage_bps: u32,
285    max_slippage_bps: u32,
286    max_order_amount: u64,
287    deadline_seconds: u64,
288    max_quote_age_blocks: u64,
289    receipt_timeout_secs: u64,
290}
291
292#[derive(Debug, Clone, Copy)]
293struct QuoteSpendCeiling {
294    spend_token: Address,
295    spend_token_decimals: u8,
296    max_amount: U256,
297}
298
299/// Execution client for blockchain interactions including balance tracking and order execution.
300#[derive(Debug)]
301pub struct BlockchainExecutionClient {
302    core: ExecutionClientCore,
303    emitter: ExecutionEventEmitter,
304    cache: BlockchainCache,
305    config: BlockchainExecutionClientConfig,
306    chain: SharedChain,
307    wallet_address: Address,
308    signer: Option<Arc<PrivateKeySigner>>,
309    payload_keys: Option<Arc<PayloadKeySet>>,
310    router_addresses: Vec<Address>,
311    transaction_limits: TransactionLimits,
312    weth_address: Address,
313    in_flight: Arc<Mutex<Option<InFlightSlot>>>,
314    wallet_balance: Arc<Mutex<WalletBalance>>,
315    erc20_contract: Erc20Contract,
316    http_rpc_client: Arc<BlockchainHttpRpcClient>,
317    verification: VerificationCoordinator,
318    pending_tasks: TaskGroup,
319}
320
321impl BlockchainExecutionClient {
322    /// Creates a new [`BlockchainExecutionClient`] instance for the specified configuration.
323    ///
324    /// # Errors
325    ///
326    /// Returns an error if transaction limits are missing or invalid, independent verification is
327    /// missing or conflicts with the configured chain, the deployment manifest or provider
328    /// topology is invalid, a configured address or token pair is invalid, the router allowlist is
329    /// empty, or the slippage bounds are inconsistent or not below 100%.
330    pub fn new(
331        core_client: ExecutionClientCore,
332        config: BlockchainExecutionClientConfig,
333    ) -> anyhow::Result<Self> {
334        let transaction_limits = Self::transaction_limits(&config)?;
335        let chain = Arc::new(config.chain.clone());
336        let cache = BlockchainCache::new(chain.clone());
337        let http_rpc_client = Arc::new(BlockchainHttpRpcClient::new(
338            config.http_rpc_url.clone(),
339            config.rpc_requests_per_second,
340            None,
341        ));
342        let verification_config = config.verification.as_ref().ok_or_else(|| {
343            anyhow::anyhow!("Independent Blockchain execution verification is required")
344        })?;
345        anyhow::ensure!(
346            verification_config.chain_anchor.chain_id == config.chain.chain_id,
347            "Verification chain anchor ID does not match the configured chain"
348        );
349        anyhow::ensure!(
350            verification_config.chain_anchor.chain_name == config.chain.name.to_string(),
351            "Verification chain anchor name does not match the configured chain"
352        );
353        let verification = VerificationCoordinator::new(
354            http_rpc_client.clone(),
355            &config.http_rpc_url,
356            verification_config,
357            config.rpc_requests_per_second,
358        )?;
359        let wallet_address = validate_address(config.wallet_address.as_str())?;
360        let erc20_contract = Erc20Contract::new_with_timeout(
361            http_rpc_client.clone(),
362            Some(EXECUTION_RPC_TIMEOUT_SECS),
363            true,
364        );
365
366        let router_addresses = config
367            .router_addresses
368            .iter()
369            .map(|address| validate_address(address.as_str()))
370            .collect::<anyhow::Result<Vec<_>>>()?;
371        if router_addresses.is_empty() {
372            anyhow::bail!("`router_addresses` must contain at least one router address");
373        }
374        let weth_address = validate_address(config.weth_address.as_str())?;
375        Self::validate_manifest_contracts(&config, &router_addresses, weth_address)?;
376
377        // Initialize token universe, so we can fetch them from the blockchain later.
378        let mut token_universe = HashSet::new();
379
380        if let Some(specified_tokens) = &config.tokens {
381            for token in specified_tokens {
382                let token_address = validate_address(token.as_str())?;
383                token_universe.insert(token_address);
384            }
385        }
386        let wallet_balance = WalletBalance::new(token_universe);
387        let emitter = ExecutionEventEmitter::new(
388            get_atomic_clock_realtime(),
389            core_client.trader_id,
390            core_client.account_id,
391            core_client.account_type,
392            core_client.base_currency,
393        );
394
395        let pending_tasks = TaskGroup::new();
396
397        Ok(Self {
398            core: core_client,
399            emitter,
400            wallet_balance: Arc::new(Mutex::new(wallet_balance)),
401            chain,
402            cache,
403            config,
404            signer: None,
405            payload_keys: None,
406            router_addresses,
407            transaction_limits,
408            weth_address,
409            in_flight: Arc::new(Mutex::new(None)),
410            erc20_contract,
411            http_rpc_client,
412            verification,
413            wallet_address,
414            pending_tasks,
415        })
416    }
417
418    fn transaction_limits(
419        config: &BlockchainExecutionClientConfig,
420    ) -> anyhow::Result<TransactionLimits> {
421        let (
422            Some(allowed_token_pairs),
423            Some(slippage_bps),
424            Some(max_slippage_bps),
425            Some(max_order_amount),
426            Some(deadline_seconds),
427            Some(max_quote_age_blocks),
428            Some(receipt_timeout_secs),
429        ) = (
430            &config.allowed_token_pairs,
431            config.slippage_bps,
432            config.max_slippage_bps,
433            config.max_order_amount,
434            config.deadline_seconds,
435            config.max_quote_age_blocks,
436            config.receipt_timeout_secs,
437        )
438        else {
439            anyhow::bail!(
440                "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"
441            );
442        };
443
444        let mut parsed_pairs = HashSet::with_capacity(allowed_token_pairs.len());
445        for (token_in, token_out) in allowed_token_pairs {
446            parsed_pairs.insert((
447                validate_address(token_in.as_str())?,
448                validate_address(token_out.as_str())?,
449            ));
450        }
451
452        let quote_spend_limits = config.quote_spend_limits.as_deref().unwrap_or_default();
453        let mut parsed_quote_spend_limits = HashMap::with_capacity(quote_spend_limits.len());
454        for limit in quote_spend_limits {
455            let token_in = validate_address(limit.token_in.as_str())?;
456            let token_out = validate_address(limit.token_out.as_str())?;
457            let spend_token = validate_address(limit.spend_token.as_str())?;
458
459            if !parsed_pairs.contains(&(token_in, token_out)) {
460                anyhow::bail!(
461                    "Quote spend limit pair {token_in} -> {token_out} is not in the `allowed_token_pairs` allowlist"
462                );
463            }
464
465            if spend_token != token_in {
466                anyhow::bail!(
467                    "Quote spend limit for {token_in} -> {token_out} is denominated in {spend_token}; `spend_token` must match `token_in`"
468                );
469            }
470
471            if limit.max_amount.is_empty()
472                || !limit.max_amount.bytes().all(|byte| byte.is_ascii_digit())
473            {
474                anyhow::bail!(
475                    "Quote spend limit `max_amount` '{}' must be a base-10 unsigned integer string",
476                    limit.max_amount
477                );
478            }
479            let max_amount = U256::from_str(&limit.max_amount).map_err(|_| {
480                anyhow::anyhow!(
481                    "Quote spend limit `max_amount` '{}' exceeds the U256 range",
482                    limit.max_amount
483                )
484            })?;
485            let ceiling = QuoteSpendCeiling {
486                spend_token,
487                spend_token_decimals: limit.spend_token_decimals,
488                max_amount,
489            };
490
491            if parsed_quote_spend_limits
492                .insert((token_in, token_out), ceiling)
493                .is_some()
494            {
495                anyhow::bail!(
496                    "Duplicate quote spend limit for token pair {token_in} -> {token_out}"
497                );
498            }
499        }
500
501        if slippage_bps > max_slippage_bps {
502            anyhow::bail!(
503                "`slippage_bps` {slippage_bps} exceeds `max_slippage_bps` {max_slippage_bps}"
504            );
505        }
506
507        if max_slippage_bps >= BPS_DENOMINATOR {
508            anyhow::bail!("`max_slippage_bps` {max_slippage_bps} must be below {BPS_DENOMINATOR}");
509        }
510
511        if !(1..=4_095).contains(&max_quote_age_blocks) {
512            anyhow::bail!("`max_quote_age_blocks` must be in 1..=4095");
513        }
514
515        Ok(TransactionLimits {
516            allowed_token_pairs: parsed_pairs,
517            quote_spend_limits: parsed_quote_spend_limits,
518            slippage_bps,
519            max_slippage_bps,
520            max_order_amount,
521            deadline_seconds,
522            max_quote_age_blocks,
523            receipt_timeout_secs,
524        })
525    }
526
527    fn validate_manifest_contracts(
528        config: &BlockchainExecutionClientConfig,
529        routers: &[Address],
530        weth: Address,
531    ) -> anyhow::Result<()> {
532        let verification = config.verification.as_ref().ok_or_else(|| {
533            anyhow::anyhow!("Independent Blockchain execution verification is required")
534        })?;
535        let manifest = &verification.deployment_manifest;
536        let role_addresses = |role| {
537            manifest
538                .contracts
539                .iter()
540                .filter(|contract| contract.role == role)
541                .map(|contract| {
542                    Address::from_str(&contract.address)
543                        .map_err(|_| anyhow::anyhow!("Deployment manifest address is invalid"))
544                })
545                .collect::<anyhow::Result<HashSet<_>>>()
546        };
547        let singleton = |role, description: &str| {
548            let addresses = role_addresses(role)?;
549            anyhow::ensure!(
550                addresses.len() == 1,
551                "Deployment manifest must contain exactly one {description} contract"
552            );
553            Ok(*addresses.iter().next().expect("singleton role address"))
554        };
555
556        let configured_routers = routers.iter().copied().collect::<HashSet<_>>();
557        anyhow::ensure!(
558            role_addresses(BlockchainContractRole::Router)? == configured_routers,
559            "Deployment manifest router set does not match `router_addresses`"
560        );
561        anyhow::ensure!(
562            singleton(BlockchainContractRole::WrappedNative, "wrapped native")? == weth,
563            "Deployment manifest wrapped native contract does not match `weth_address`"
564        );
565        let factory = singleton(BlockchainContractRole::Factory, "factory")?;
566        let registered_factory =
567            crate::exchanges::get_dex_extended(config.chain.name, &DexType::UniswapV3)
568                .map(|dex| dex.factory)
569                .ok_or_else(|| {
570                    anyhow::anyhow!(
571                        "No registered Uniswap V3 deployment for chain {}",
572                        config.chain.name
573                    )
574                })?;
575        anyhow::ensure!(
576            factory == registered_factory,
577            "Deployment manifest factory does not match the registered Uniswap V3 factory"
578        );
579        let quote_contract = singleton(BlockchainContractRole::Quote, "quote")?;
580
581        let mut token_decimals = HashMap::new();
582
583        for token in &manifest.tokens {
584            let address = Address::from_str(&token.address)
585                .map_err(|_| anyhow::anyhow!("Deployment manifest token address is invalid"))?;
586            anyhow::ensure!(
587                token_decimals.insert(address, token.decimals).is_none(),
588                "Deployment manifest contains a duplicate token identity"
589            );
590        }
591        anyhow::ensure!(
592            token_decimals.contains_key(&weth),
593            "Deployment manifest has no wrapped native token identity"
594        );
595
596        if let Some(tokens) = &config.tokens {
597            for token in tokens {
598                let address = validate_address(token)?;
599                anyhow::ensure!(
600                    token_decimals.contains_key(&address),
601                    "Configured token {address} has no deployment manifest identity"
602                );
603            }
604        }
605
606        for (token_in, token_out) in config.allowed_token_pairs.as_deref().unwrap_or_default() {
607            let token_in = validate_address(token_in)?;
608            let token_out = validate_address(token_out)?;
609            anyhow::ensure!(
610                token_in != token_out
611                    && token_decimals.contains_key(&token_in)
612                    && token_decimals.contains_key(&token_out),
613                "Allowed token pair {token_in} -> {token_out} is not fully pinned by the deployment manifest"
614            );
615        }
616
617        for limit in config.quote_spend_limits.as_deref().unwrap_or_default() {
618            let spend_token = validate_address(&limit.spend_token)?;
619            anyhow::ensure!(
620                token_decimals.get(&spend_token) == Some(&limit.spend_token_decimals),
621                "Quote spend limit decimals do not match the deployment manifest"
622            );
623        }
624
625        let pool_contracts = role_addresses(BlockchainContractRole::Pool)?;
626
627        for pool in &manifest.pools {
628            let pool_address = Address::from_str(&pool.address)
629                .map_err(|_| anyhow::anyhow!("Deployment manifest pool address is invalid"))?;
630            let pool_factory = Address::from_str(&pool.factory)
631                .map_err(|_| anyhow::anyhow!("Deployment manifest pool factory is invalid"))?;
632            let pool_quote = Address::from_str(&pool.quote_contract).map_err(|_| {
633                anyhow::anyhow!("Deployment manifest pool quote contract is invalid")
634            })?;
635            anyhow::ensure!(
636                pool_contracts.contains(&pool_address)
637                    && pool_factory == factory
638                    && pool_quote == quote_contract,
639                "Deployment manifest pool does not use the pinned pool, factory, and quote identities"
640            );
641        }
642        Ok(())
643    }
644
645    /// Fetches the native currency balance (e.g., ETH) for the wallet from the blockchain.
646    async fn fetch_native_currency_balance(&self) -> anyhow::Result<Money> {
647        let balance_u256 = self
648            .http_rpc_client
649            .get_balance_with_timeout(&self.wallet_address, None, Some(EXECUTION_RPC_TIMEOUT_SECS))
650            .await?;
651
652        let native_currency = self.chain.native_currency();
653
654        Money::from_u256(balance_u256, native_currency).map_err(Into::into)
655    }
656
657    /// Fetches the balance of a specific ERC-20 token for the wallet.
658    async fn fetch_token_balance(
659        &mut self,
660        token_address: &Address,
661    ) -> anyhow::Result<TokenBalance> {
662        // Get the cached token or fetch it from the blockchain and cache it.
663        let token = if let Some(token) = self.cache.get_token(token_address) {
664            token.to_owned()
665        } else {
666            let token_info = self.erc20_contract.fetch_token_info(token_address).await?;
667            let token = Token::new(
668                self.chain.clone(),
669                *token_address,
670                token_info.name,
671                token_info.symbol,
672                token_info.decimals,
673            );
674            self.cache.add_token(token.clone()).await?;
675            token
676        };
677
678        let amount = self
679            .erc20_contract
680            .balance_of(token_address, &self.wallet_address)
681            .await?;
682        let token_balance = TokenBalance::new(amount, token);
683
684        // TODO: Use price oracle here and cache, to get the latest price then convert to USD
685        // then use token_balance.set_amount_usd(amount_usd) to set the amount_usd value.
686
687        Ok(token_balance)
688    }
689
690    /// Refreshes and publishes all native currency and tracked ERC-20 balances.
691    async fn refresh_wallet_balances(&mut self) -> anyhow::Result<()> {
692        let (wallet_balance, balances) = self.fetch_wallet_balances().await?;
693        self.generate_account_state(
694            balances,
695            vec![],
696            true,
697            get_atomic_clock_realtime().get_time_ns(),
698            None,
699        )?;
700        *self.wallet_balance.lock() = wallet_balance;
701        Ok(())
702    }
703
704    async fn fetch_wallet_balances(
705        &mut self,
706    ) -> anyhow::Result<(WalletBalance, Vec<AccountBalance>)> {
707        let native_currency_balance = self.fetch_native_currency_balance().await?;
708        let token_universe = self.wallet_balance.lock().token_universe.clone();
709        let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
710        token_addresses.sort_unstable();
711
712        let mut token_balances = Vec::with_capacity(token_addresses.len());
713        for token_address in token_addresses {
714            let token_balance = self
715                .fetch_token_balance(&token_address)
716                .await
717                .with_context(|| format!("failed to fetch token balance for {token_address}"))?;
718            token_balances.push(token_balance);
719        }
720
721        let mut wallet_balance = WalletBalance::new(token_universe);
722        let balances = wallet_balance.replace_balances(native_currency_balance, token_balances)?;
723        log::debug!(
724            "Refreshed wallet balance with {} account balances",
725            balances.len()
726        );
727        Ok((wallet_balance, balances))
728    }
729
730    /// Runs a read-only execution preflight for the pool selected by `instrument_id`.
731    ///
732    /// Verifies the RPC chain ID against configuration, deployed bytecode at the router, pool,
733    /// and token addresses, wallet native and token balances, exact router allowance of the
734    /// pool's base (input) token, and current fee conditions. Changes no state.
735    ///
736    /// # Errors
737    ///
738    /// Returns an error if the instrument ID cannot be resolved to a cached Uniswap V3 pool
739    /// on the client's chain, or if any RPC call fails. On-chain readiness failures are
740    /// reported in the returned [`BlockchainPreflightReport`], not as errors.
741    pub async fn preflight(
742        &self,
743        instrument_id: &InstrumentId,
744    ) -> anyhow::Result<BlockchainPreflightReport> {
745        let pool = self.resolve_pool(instrument_id)?;
746        let base_token = pool.get_base_token().clone();
747        let quote_token = pool.get_quote_token().clone();
748
749        let actual_chain_id = self.http_rpc_client.chain_id().await?;
750
751        let pool_code = self.http_rpc_client.get_code(&pool.address).await?;
752        let pool_check = PoolPreflightCheck {
753            instrument_id: pool.instrument_id,
754            address: pool.address,
755            has_deployed_code: !pool_code.is_empty(),
756            fee: pool.fee,
757            base_token: base_token.address,
758            quote_token: quote_token.address,
759        };
760
761        let mut routers = Vec::with_capacity(self.router_addresses.len());
762        for router in &self.router_addresses {
763            let code = self.http_rpc_client.get_code(router).await?;
764            routers.push(ContractCodeCheck {
765                address: *router,
766                has_deployed_code: !code.is_empty(),
767            });
768        }
769
770        let mut tokens = Vec::with_capacity(2);
771
772        for token in [&base_token, &quote_token] {
773            let code = self.http_rpc_client.get_code(&token.address).await?;
774            let wallet_balance = self
775                .erc20_contract
776                .balance_of(&token.address, &self.wallet_address)
777                .await?;
778
779            let mut router_allowances = Vec::new();
780            if token.address == base_token.address {
781                router_allowances.reserve(self.router_addresses.len());
782                for router in &self.router_addresses {
783                    let amount = self
784                        .erc20_contract
785                        .allowance(&token.address, &self.wallet_address, router)
786                        .await?;
787                    router_allowances.push((*router, amount));
788                }
789            }
790
791            tokens.push(TokenPreflightCheck {
792                address: token.address,
793                symbol: token.symbol.clone(),
794                has_deployed_code: !code.is_empty(),
795                wallet_balance,
796                router_allowances,
797            });
798        }
799
800        let native_balance_wei = self
801            .http_rpc_client
802            .get_balance_with_timeout(&self.wallet_address, None, Some(EXECUTION_RPC_TIMEOUT_SECS))
803            .await?;
804
805        let latest_block = self.http_rpc_client.latest_block().await?;
806        let base_fee_per_gas_wei = latest_block.base_fee_per_gas.ok_or_else(|| {
807            anyhow::anyhow!("Latest block {} has no base fee", latest_block.number)
808        })?;
809        let max_priority_fee_per_gas_wei = self.http_rpc_client.max_priority_fee_per_gas().await?;
810        let derived_max_fee_per_gas_wei = compute_max_fee(
811            base_fee_per_gas_wei,
812            max_priority_fee_per_gas_wei,
813            self.config.base_fee_buffer_bps,
814        )?;
815
816        Ok(BlockchainPreflightReport::new(
817            u64::from(self.chain.chain_id),
818            actual_chain_id,
819            pool_check,
820            routers,
821            tokens,
822            native_balance_wei,
823            base_fee_per_gas_wei,
824            max_priority_fee_per_gas_wei,
825            derived_max_fee_per_gas_wei,
826            u128::from(self.config.max_fee_per_gas_wei),
827        ))
828    }
829
830    /// Wraps native currency into the wrapped native token via a WETH `deposit()`
831    /// transaction carrying `amount_wei` of native value.
832    ///
833    /// This is an explicit operator operation; it never runs inside `submit_order`.
834    ///
835    /// # Errors
836    ///
837    /// Returns an error if the amount is zero, the WETH target is not a deployed ERC-20 contract,
838    /// the client is not connected, another transaction is in flight, no durable store is
839    /// configured, the WETH balance does not increase by `amount_wei`, or any RPC, policy, signing,
840    /// persistence, or broadcast step fails. A persistence failure after signing, or a failed
841    /// postcondition after finality, leaves the in-flight slot occupied.
842    pub async fn wrap(&mut self, amount_wei: U256) -> anyhow::Result<B256> {
843        if amount_wei.is_zero() {
844            anyhow::bail!("Wrap amount must be positive");
845        }
846
847        self.ensure_transaction_ready(TransactionPurpose::Wrap)?;
848
849        let calldata = WETH9::depositCall {}.abi_encode();
850        let executor = self.transaction_executor()?;
851        let included = executor
852            .transact(
853                self.weth_address,
854                amount_wei,
855                Bytes::from(calldata),
856                TransactionPurpose::Wrap,
857                None,
858                TransactionAuthorization::Wrap {
859                    weth: self.weth_address,
860                },
861            )
862            .await?;
863        let postconditions =
864            verify_wrap_balance_increase(&executor, &self.weth_address, amount_wei, &included)
865                .await?;
866        executor
867            .commit_verified_finality(&included, TransactionStatus::Finalized, &postconditions)
868            .await?;
869        executor
870            .database
871            .mark_execution_event_emitted(included.intent_id, "terminal")
872            .await?;
873        executor.release_slot();
874
875        Ok(included.tx_hash)
876    }
877
878    /// Approves an allowlisted SwapRouter to spend `amount` of `token` via an ERC-20
879    /// `approve` transaction. Zero always revokes the allowance. For nonzero requests,
880    /// `unlimited_approval` changes the target to `U256::MAX`.
881    ///
882    /// This is an explicit operator operation; it never runs inside `submit_order`.
883    ///
884    /// # Errors
885    ///
886    /// Returns an error if the router or token fails policy and deployment checks, a nonzero
887    /// allowance was not cleared first, approval simulation returns false or malformed data, the
888    /// client is not connected, another transaction is in flight, no durable store is configured,
889    /// the resulting allowance differs from the target, or any RPC, signing, persistence, or
890    /// broadcast step fails. A persistence failure after signing, or a failed postcondition after
891    /// finality, leaves the in-flight slot occupied.
892    pub async fn approve(
893        &mut self,
894        token: Address,
895        amount: U256,
896        router: Address,
897    ) -> anyhow::Result<B256> {
898        if !self.router_addresses.contains(&router) {
899            anyhow::bail!("Router {router} is not in the configured `router_addresses` allowlist");
900        }
901
902        if !amount.is_zero()
903            && !self
904                .transaction_limits
905                .allowed_token_pairs
906                .iter()
907                .any(|(token_in, _)| *token_in == token)
908        {
909            anyhow::bail!(
910                "Token {token} is not an input token in the configured `allowed_token_pairs`"
911            );
912        }
913
914        self.ensure_transaction_ready(TransactionPurpose::Approve)?;
915
916        let approval_amount = if amount.is_zero() {
917            U256::ZERO
918        } else if self.config.unlimited_approval {
919            U256::MAX
920        } else {
921            amount
922        };
923        let calldata = ERC20::approveCall {
924            spender: router,
925            amount: approval_amount,
926        }
927        .abi_encode();
928
929        let executor = self.transaction_executor()?;
930        let included = executor
931            .transact(
932                token,
933                U256::ZERO,
934                Bytes::from(calldata),
935                TransactionPurpose::Approve,
936                None,
937                TransactionAuthorization::Approve {
938                    token,
939                    router,
940                    amount: approval_amount,
941                },
942            )
943            .await?;
944        let postconditions =
945            verify_approve_allowance(&executor, &token, &router, approval_amount, &included)
946                .await?;
947        executor
948            .commit_verified_finality(&included, TransactionStatus::Finalized, &postconditions)
949            .await?;
950        executor
951            .database
952            .mark_execution_event_emitted(included.intent_id, "terminal")
953            .await?;
954        executor.release_slot();
955
956        Ok(included.tx_hash)
957    }
958
959    fn uniswap_v3_factory(&self) -> anyhow::Result<Address> {
960        crate::exchanges::get_dex_extended(self.chain.name, &DexType::UniswapV3)
961            .map(|dex| dex.factory)
962            .ok_or_else(|| {
963                anyhow::anyhow!(
964                    "No registered Uniswap V3 deployment for chain {}",
965                    self.chain.name
966                )
967            })
968    }
969
970    /// Resolves the pool selected by `instrument_id` from the shared engine cache.
971    fn resolve_pool(&self, instrument_id: &InstrumentId) -> anyhow::Result<Pool> {
972        let (blockchain, dex_type) = instrument_id.venue.parse_dex()?;
973        if blockchain != self.chain.name {
974            anyhow::bail!(
975                "Pool venue chain {blockchain} does not match the client chain {}",
976                self.chain.name
977            );
978        }
979
980        if dex_type != DexType::UniswapV3 {
981            anyhow::bail!("Unsupported DEX type {dex_type}; only UniswapV3 is supported");
982        }
983
984        let pool_identifier = PoolIdentifier::new_checked(instrument_id.symbol.as_str())?;
985        if !pool_identifier.is_address() {
986            anyhow::bail!(
987                "Pool identifier {pool_identifier} is a pool ID; only address identifiers are supported"
988            );
989        }
990
991        let pool = self
992            .core
993            .cache()
994            .pool(instrument_id)
995            .cloned()
996            .ok_or_else(|| {
997                anyhow::anyhow!(
998                    "Unknown pool {instrument_id}; not found in the shared engine cache"
999                )
1000            })?;
1001
1002        if pool.token0.get_token_priority() == pool.token1.get_token_priority() {
1003            anyhow::bail!(
1004                "Pool {instrument_id} tokens share a token priority; base and quote orientation is ambiguous"
1005            );
1006        }
1007
1008        Ok(pool)
1009    }
1010
1011    /// Authenticates every persisted signed transaction in this execution database.
1012    ///
1013    /// Run this while the execution client is disconnected. The check takes a stable table lock,
1014    /// reads in bounded batches, and returns counts, deployment identity, key IDs, and database
1015    /// roles with direct ownership or `SELECT` grants.
1016    ///
1017    /// # Errors
1018    ///
1019    /// Returns an error for inconsistent storage state, missing keys, or any payload which cannot
1020    /// be opened and authenticated against its durable intent.
1021    pub async fn check_payload_storage(
1022        &self,
1023        batch_size: usize,
1024    ) -> anyhow::Result<PayloadStorageCheck> {
1025        let batch_size = validate_payload_operation_batch_size(batch_size)?;
1026        let database = self.payload_operation_database().await?;
1027        let keys = self.load_payload_keys()?;
1028        database
1029            .check_execution_payload_storage(keys.as_ref(), None, batch_size)
1030            .await
1031            .map(Into::into)
1032    }
1033
1034    /// Activates or resumes protected signed-transaction storage for this execution database.
1035    ///
1036    /// Run this while the execution client is disconnected. A full payload check must succeed
1037    /// before a later execution connection is permitted.
1038    ///
1039    /// # Errors
1040    ///
1041    /// Returns an error if the client is connected, an active key or deployment identity is
1042    /// unavailable, or any stored payload cannot be migrated and authenticated.
1043    pub async fn protect_payload_storage(&self) -> anyhow::Result<()> {
1044        let database = self.payload_operation_database().await?;
1045        database.ensure_execution_transaction_schema().await?;
1046        let keys = self
1047            .load_payload_keys()?
1048            .ok_or_else(|| anyhow::anyhow!("Payload protection requires an active payload key"))?;
1049        database.ensure_execution_payload_storage(&keys).await
1050    }
1051
1052    /// Rewraps all protected payloads in this execution database with the configured active key.
1053    ///
1054    /// The prior active key must remain configured as a retired key until this method and a
1055    /// subsequent full check both succeed.
1056    ///
1057    /// # Errors
1058    ///
1059    /// Returns an error if the client is connected, storage is not protected, required keys are
1060    /// unavailable, or any bounded rewrap batch fails authentication.
1061    pub async fn rewrap_payload_storage(&self, batch_size: usize) -> anyhow::Result<()> {
1062        let batch_size = validate_payload_operation_batch_size(batch_size)?;
1063        let database = self.payload_operation_database().await?;
1064        let keys = self
1065            .load_payload_keys()?
1066            .ok_or_else(|| anyhow::anyhow!("Payload rewrap requires an active payload key"))?;
1067        database
1068            .rewrap_execution_payload_storage(&keys, batch_size)
1069            .await
1070    }
1071
1072    /// Restores authenticated plaintext payloads and removes protection from this database.
1073    ///
1074    /// This incident-only operation is resumable. Keep the complete key set configured until it
1075    /// succeeds and the unprotected database passes a full payload check.
1076    ///
1077    /// # Errors
1078    ///
1079    /// Returns an error if the client is connected, storage is not protected, required keys are
1080    /// unavailable, or any bounded rollback batch fails authentication.
1081    pub async fn rollback_payload_storage(&self, batch_size: usize) -> anyhow::Result<()> {
1082        let batch_size = validate_payload_operation_batch_size(batch_size)?;
1083        let database = self.payload_operation_database().await?;
1084        let keys = self
1085            .load_payload_keys()?
1086            .ok_or_else(|| anyhow::anyhow!("Payload rollback requires an active payload key"))?;
1087        database
1088            .rollback_execution_payload_storage(&keys, batch_size)
1089            .await
1090    }
1091
1092    async fn payload_operation_database(&self) -> anyhow::Result<BlockchainCacheDatabase> {
1093        anyhow::ensure!(
1094            !self.core.is_connected(),
1095            "Disconnect the execution client before payload storage operations"
1096        );
1097
1098        if let Some(database) = &self.cache.database {
1099            return Ok(database.clone());
1100        }
1101        let options = self
1102            .config
1103            .postgres_cache_database_config
1104            .as_ref()
1105            .ok_or_else(|| anyhow::anyhow!("No Postgres cache database is configured"))?;
1106        BlockchainCacheDatabase::connect(options.clone().into())
1107            .await
1108            .context("failed to connect to the execution database")
1109    }
1110
1111    fn load_payload_keys(&self) -> anyhow::Result<Option<PayloadKeySet>> {
1112        PayloadKeySet::load(
1113            self.config.payload_key_env.as_deref(),
1114            &self.config.payload_key_retired_env,
1115            self.config.payload_deployment_id.as_deref(),
1116        )
1117    }
1118
1119    fn payload_policy(&self) -> PayloadPolicy {
1120        PayloadPolicy {
1121            chain_id: self.chain.chain_id,
1122            signer: self.wallet_address,
1123            gas_limit: self.config.gas_limit,
1124            max_fee_per_gas: self.config.max_fee_per_gas_wei,
1125        }
1126    }
1127
1128    /// Builds the shared transaction executor from the connected client state.
1129    ///
1130    /// # Errors
1131    ///
1132    /// Returns an error if no durable store is configured or the signer is not initialized.
1133    fn transaction_executor(&self) -> anyhow::Result<TransactionExecutor> {
1134        let database = self.cache.database.clone().ok_or_else(|| {
1135            anyhow::anyhow!("No durable store configured; refusing to submit a transaction")
1136        })?;
1137        let signer = self
1138            .signer
1139            .clone()
1140            .ok_or_else(|| anyhow::anyhow!("Signer not initialized; connect the client first"))?;
1141        let payload_keys = self.payload_keys.clone().ok_or_else(|| {
1142            anyhow::anyhow!("Protected payload keys are not initialized; connect the client first")
1143        })?;
1144        let verification_config = self
1145            .config
1146            .verification
1147            .as_ref()
1148            .expect("verification config validated at construction");
1149        let identities = std::iter::once(&verification_config.authoritative)
1150            .chain(
1151                verification_config
1152                    .verifiers
1153                    .iter()
1154                    .map(|provider| &provider.identity),
1155            )
1156            .collect::<Vec<_>>();
1157
1158        Ok(TransactionExecutor {
1159            http_rpc_client: self.http_rpc_client.clone(),
1160            verification: self.verification.clone(),
1161            manifest_version: verification_config.manifest_version.clone(),
1162            manifest_digest: verification_config.manifest_digest.clone(),
1163            deployment_manifest: Arc::new(verification_config.deployment_manifest.clone()),
1164            provider_ids: identities
1165                .iter()
1166                .map(|identity| identity.provider_id.clone())
1167                .collect(),
1168            operator_ids: identities
1169                .iter()
1170                .map(|identity| identity.operator_id.clone())
1171                .collect(),
1172            failure_domain_ids: identities
1173                .iter()
1174                .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
1175                .collect(),
1176            database,
1177            signer,
1178            payload_keys,
1179            in_flight: Arc::clone(&self.in_flight),
1180            wallet_balance: Arc::clone(&self.wallet_balance),
1181            account_id: self.core.account_id,
1182            wallet_address: self.wallet_address,
1183            chain_id: self.chain.chain_id,
1184            max_fee_per_gas_wei: self.config.max_fee_per_gas_wei,
1185            base_fee_buffer_bps: self.config.base_fee_buffer_bps,
1186            gas_limit: self.config.gas_limit,
1187            gas_buffer_bps: self.config.gas_buffer_bps,
1188            receipt_timeout: receipt_timeout(self.transaction_limits.receipt_timeout_secs),
1189            receipt_max_polls: receipt_max_polls(self.transaction_limits.receipt_timeout_secs),
1190        })
1191    }
1192
1193    fn restore_swap_plan(&self, intent: &ExecutionIntentRow) -> anyhow::Result<SwapPlan> {
1194        let client_order_id = ClientOrderId::new_checked(
1195            intent
1196                .client_order_id
1197                .as_deref()
1198                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no client order ID"))?,
1199        )?;
1200        let order = self
1201            .core
1202            .cache()
1203            .try_order_owned(&client_order_id)
1204            .with_context(|| {
1205                format!(
1206                    "Cannot reconcile swap intent {} because order {client_order_id} is not restored",
1207                    intent.id
1208                )
1209            })?;
1210        let instrument_id = InstrumentId::from_str(
1211            intent
1212                .instrument_id
1213                .as_deref()
1214                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no instrument ID"))?,
1215        )?;
1216        anyhow::ensure!(
1217            order.instrument_id() == instrument_id,
1218            "Persisted swap instrument {instrument_id} does not match restored order instrument {}",
1219            order.instrument_id()
1220        );
1221        anyhow::ensure!(
1222            intent.trader_id.as_deref() == Some(order.trader_id().as_str()),
1223            "Persisted swap trader does not match restored order"
1224        );
1225        anyhow::ensure!(
1226            intent.strategy_id.as_deref() == Some(order.strategy_id().as_str()),
1227            "Persisted swap strategy does not match restored order"
1228        );
1229        anyhow::ensure!(
1230            intent.account_id.as_deref() == Some(self.core.account_id.as_str()),
1231            "Persisted swap account does not match execution client account"
1232        );
1233
1234        let pool = self.resolve_pool(&instrument_id)?;
1235        let pool_address = Address::from_str(
1236            intent
1237                .pool_address
1238                .as_deref()
1239                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no pool address"))?,
1240        )?;
1241        anyhow::ensure!(
1242            pool.address == pool_address,
1243            "Persisted pool {pool_address} does not match restored pool {}",
1244            pool.address
1245        );
1246        let amount_in = U256::from_str(
1247            intent
1248                .amount_in
1249                .as_deref()
1250                .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no input amount"))?,
1251        )?;
1252        let fee = U24::try_from(
1253            pool.fee
1254                .ok_or_else(|| anyhow::anyhow!("Restored pool {instrument_id} has no fee"))?,
1255        )?;
1256        let quote_token = pool.get_quote_token();
1257        let quote_currency = Currency::new_checked(
1258            &quote_token.symbol,
1259            quote_token.decimals,
1260            0,
1261            &quote_token.name,
1262            CurrencyType::Crypto,
1263        )?;
1264        let (token_in, token_out) = swap_token_pair(
1265            order.order_side(),
1266            pool.get_base_token().address,
1267            quote_token.address,
1268        )?;
1269        let factory = self.uniswap_v3_factory()?;
1270        anyhow::ensure!(
1271            pool.dex.factory == factory,
1272            "Restored pool {instrument_id} references factory {}, expected registered factory {factory}",
1273            pool.dex.factory
1274        );
1275
1276        Ok(SwapPlan {
1277            order,
1278            quote_currency,
1279            pool,
1280            instrument_id,
1281            pool_address,
1282            router: Address::from_str(&intent.transaction_to)?,
1283            factory,
1284            weth: self.weth_address,
1285            token_in,
1286            token_out,
1287            fee,
1288            amount_in,
1289            min_amount_out: U256::ZERO,
1290            slippage_bps: 0,
1291            quote_spend_ceiling: None,
1292            profiler_position: None,
1293        })
1294    }
1295
1296    async fn reconcile_unresolved_execution(&self) -> anyhow::Result<()> {
1297        let database = self.cache.database.clone().ok_or_else(|| {
1298            anyhow::anyhow!("No durable store configured for execution reconciliation")
1299        })?;
1300        let _payload_lease = database
1301            .acquire_execution_payload_lease(self.payload_keys.as_deref().ok_or_else(|| {
1302                anyhow::anyhow!("Protected payload keys are required for execution recovery")
1303            })?)
1304            .await?;
1305        let wallet_address = self.wallet_address.to_string();
1306        anyhow::ensure!(
1307            !database
1308                .has_recoverable_signed_execution(self.chain.chain_id, &wallet_address)
1309                .await?,
1310            "A recoverable execution for wallet {} retains signed transaction bytes; refusing to reuse its nonce without explicit recovery",
1311            self.wallet_address
1312        );
1313        let Some(intent) = database
1314            .get_active_execution_intent(self.chain.chain_id, &wallet_address)
1315            .await?
1316        else {
1317            return Ok(());
1318        };
1319        anyhow::ensure!(
1320            intent.schema_version == crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
1321            "Execution intent {} uses unsupported schema version {}",
1322            intent.id,
1323            intent.schema_version
1324        );
1325
1326        if intent.status == "prepared" {
1327            database
1328                .mark_execution_intent_recoverable(intent.id)
1329                .await?;
1330            release_preparing_slot(&self.in_flight);
1331            return Ok(());
1332        }
1333
1334        let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
1335            anyhow::anyhow!(
1336                "Execution intent {} has unknown purpose {}",
1337                intent.id,
1338                intent.purpose
1339            )
1340        })?;
1341        let nonce = intent
1342            .nonce
1343            .ok_or_else(|| anyhow::anyhow!("Active execution intent {} has no nonce", intent.id))?;
1344        *self.in_flight.lock() = Some(InFlightSlot::Recovering(RecoveryTransaction {
1345            intent_id: intent.id,
1346            nonce,
1347            purpose,
1348        }));
1349        let hashes = database.get_execution_transaction_hashes(intent.id).await?;
1350        let current = current_execution_hash(intent.id, &hashes)?;
1351        let tx_hash = B256::from_str(&current.transaction_hash).with_context(|| {
1352            format!(
1353                "Execution intent {} has invalid transaction hash {}",
1354                intent.id, current.transaction_hash
1355            )
1356        })?;
1357        let policy = PayloadPolicy {
1358            chain_id: self.chain.chain_id,
1359            signer: self.wallet_address,
1360            gas_limit: self.config.gas_limit,
1361            max_fee_per_gas: self.config.max_fee_per_gas_wei,
1362        };
1363        let mut authenticated_payloads = HashMap::new();
1364        let mut current_payload = None;
1365
1366        for hash in &hashes {
1367            anyhow::ensure!(
1368                hash.intent_id == intent.id,
1369                "Persisted transaction row references intent {}, expected {}",
1370                hash.intent_id,
1371                intent.id
1372            );
1373            anyhow::ensure!(
1374                hash.chain_id == self.chain.chain_id,
1375                "Persisted transaction row chain ID {} does not match configured chain ID {}",
1376                hash.chain_id,
1377                self.chain.chain_id
1378            );
1379
1380            if !hash.payload_expected {
1381                anyhow::ensure!(
1382                    hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
1383                    "Replacement transaction {} unexpectedly retains signed bytes",
1384                    hash.transaction_hash
1385                );
1386                continue;
1387            }
1388            let raw_transaction = open_execution_payload(
1389                self.payload_keys
1390                    .as_deref()
1391                    .expect("payload keys checked above"),
1392                policy,
1393                &intent,
1394                hash,
1395                "recovery",
1396            )
1397            .map_err(|e| {
1398                anyhow::anyhow!(
1399                    "Execution intent {} signed transaction {} failed authentication: {e}",
1400                    intent.id,
1401                    hash.transaction_hash
1402                )
1403            })?;
1404            let authenticated_hash = B256::from_str(&hash.transaction_hash).with_context(|| {
1405                format!(
1406                    "Execution intent {} has invalid transaction hash {}",
1407                    intent.id, hash.transaction_hash
1408                )
1409            })?;
1410            anyhow::ensure!(
1411                authenticated_payloads
1412                    .insert(authenticated_hash, raw_transaction.clone())
1413                    .is_none(),
1414                "Execution intent {} has duplicate authenticated transaction hash {}",
1415                intent.id,
1416                hash.transaction_hash
1417            );
1418
1419            if hash.id == current.id {
1420                current_payload = Some(raw_transaction);
1421            }
1422        }
1423        anyhow::ensure!(
1424            !authenticated_payloads.is_empty(),
1425            "Execution intent {} has no persisted signed transaction bytes",
1426            intent.id
1427        );
1428
1429        if intent.status == "broadcast" {
1430            anyhow::ensure!(
1431                current_payload.is_some(),
1432                "Broadcast execution intent {} has no persisted signed transaction bytes",
1433                intent.id
1434            );
1435        }
1436
1437        if intent.status == "signed" {
1438            anyhow::bail!(
1439                "Execution intent {} has a signed transaction {} that was not authorized for broadcast; its nonce remains reserved pending explicit recovery",
1440                intent.id,
1441                tx_hash
1442            );
1443        }
1444
1445        *self.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
1446            intent_id: intent.id,
1447            nonce,
1448            tx_hash,
1449            purpose,
1450        }));
1451
1452        let plan = if purpose == TransactionPurpose::Swap {
1453            Some(self.restore_swap_plan(&intent)?)
1454        } else {
1455            None
1456        };
1457
1458        if let Some(plan) = &plan
1459            && !intent.acknowledgement_emitted
1460        {
1461            if plan.order.ts_submitted().is_none() {
1462                self.emitter.emit_order_submitted(&plan.order);
1463            }
1464            database
1465                .mark_execution_event_emitted(intent.id, "acknowledgement")
1466                .await?;
1467        }
1468
1469        let executor = self.transaction_executor()?;
1470        let mut prepared = PreparedTransaction {
1471            intent_id: intent.id,
1472            created_block: intent.created_block,
1473            nonce,
1474            tx_hash,
1475            raw_tx: current_payload.unwrap_or_default(),
1476            payload_lease: None,
1477        };
1478
1479        let finality_already_committed = matches!(intent.status.as_str(), "finalized" | "reverted");
1480        if !finality_already_committed {
1481            match executor
1482                .authorize_rebroadcast(&prepared, &intent, purpose)
1483                .await?
1484            {
1485                ReconciliationAuthorization::Rebroadcast => {
1486                    match executor.broadcast(&prepared).await? {
1487                        BroadcastOutcome::Accepted => {}
1488                        BroadcastOutcome::Ambiguous(message) => log::warn!("{message}"),
1489                    }
1490                }
1491                ReconciliationAuthorization::Retain => {
1492                    log::warn!(
1493                        "Rebroadcast of transaction {} was suppressed by verified reconciliation state",
1494                        prepared.tx_hash
1495                    );
1496                }
1497                ReconciliationAuthorization::ScanReplacement(head) => {
1498                    let Some((replacement_hash, replacement_payload)) = executor
1499                        .scan_canonical_replacement(&intent, nonce, head, &authenticated_payloads)
1500                        .await?
1501                    else {
1502                        log::warn!(
1503                            "Canonical replacement scan for intent {} reached its bounded verified window",
1504                            intent.id
1505                        );
1506                        return Ok(());
1507                    };
1508                    prepared.tx_hash = replacement_hash;
1509                    prepared.raw_tx = replacement_payload;
1510                    *self.in_flight.lock() =
1511                        Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
1512                            intent_id: intent.id,
1513                            nonce,
1514                            tx_hash: replacement_hash,
1515                            purpose,
1516                        }));
1517                }
1518            }
1519        }
1520        let outcome = if finality_already_committed {
1521            let receipt = verified_value(
1522                executor.verification.verify_receipt(&tx_hash).await,
1523                "persisted terminal receipt",
1524            )?;
1525            let finality = executor
1526                .receipt_is_stably_finalized(&receipt)
1527                .await?
1528                .ok_or_else(|| {
1529                    anyhow::anyhow!(
1530                        "Persisted terminal transaction {tx_hash} is not stable at the finalized boundary"
1531                    )
1532                })?;
1533            let included = IncludedTransaction {
1534                intent_id: intent.id,
1535                nonce,
1536                tx_hash,
1537                block_number: receipt.block_number,
1538                receipt,
1539                finality,
1540            };
1541
1542            if intent.status == "finalized" {
1543                InclusionOutcome::Finalized(included)
1544            } else {
1545                InclusionOutcome::Reverted(included)
1546            }
1547        } else {
1548            executor.await_finality(&prepared).await?
1549        };
1550
1551        match outcome {
1552            InclusionOutcome::Finalized(mut included) => {
1553                let trace_purpose = match (&plan, purpose) {
1554                    (Some(plan), TransactionPurpose::Swap) => match plan.order.order_side() {
1555                        OrderSide::Sell => "swap_sell",
1556                        OrderSide::Buy => "swap_buy",
1557                    },
1558                    (None, TransactionPurpose::Wrap) => "wrap",
1559                    (None, TransactionPurpose::Approve) => "approve",
1560                    _ => anyhow::bail!("Restored transaction purpose is inconsistent"),
1561                };
1562                included.finality.decisions.extend(
1563                    verify_finalized_transaction(
1564                        &included,
1565                        &intent,
1566                        nonce,
1567                        &prepared.raw_tx,
1568                        &executor,
1569                        trace_purpose,
1570                    )
1571                    .await?,
1572                );
1573
1574                if let Some(plan) = plan {
1575                    let fill = validate_finalized_swap_fill(&plan, &included)?;
1576                    let wallet =
1577                        load_verified_wallet_after_fill(&plan, &included, &executor).await?;
1578                    if !finality_already_committed {
1579                        executor
1580                            .commit_verified_finality(
1581                                &included,
1582                                TransactionStatus::Finalized,
1583                                &wallet.decisions,
1584                            )
1585                            .await?;
1586                    }
1587                    complete_finalized_swap(
1588                        &plan,
1589                        intent.id,
1590                        included.tx_hash,
1591                        fill,
1592                        wallet,
1593                        &executor,
1594                        &self.emitter,
1595                    )
1596                    .await?;
1597                    executor.release_slot();
1598                } else {
1599                    let postconditions = self
1600                        .verify_recovered_operator_transaction(
1601                            &intent, purpose, &included, &executor,
1602                        )
1603                        .await?;
1604
1605                    if !finality_already_committed {
1606                        executor
1607                            .commit_verified_finality(
1608                                &included,
1609                                TransactionStatus::Finalized,
1610                                &postconditions,
1611                            )
1612                            .await?;
1613                    }
1614                    database
1615                        .mark_execution_event_emitted(intent.id, "terminal")
1616                        .await?;
1617                    executor.release_slot();
1618                }
1619            }
1620            InclusionOutcome::Reverted(mut included) => {
1621                let trace_purpose = match (&plan, purpose) {
1622                    (Some(plan), TransactionPurpose::Swap) => match plan.order.order_side() {
1623                        OrderSide::Sell => "swap_sell",
1624                        OrderSide::Buy => "swap_buy",
1625                    },
1626                    (None, TransactionPurpose::Wrap) => "wrap",
1627                    (None, TransactionPurpose::Approve) => "approve",
1628                    _ => anyhow::bail!("Restored transaction purpose is inconsistent"),
1629                };
1630                included.finality.decisions.extend(
1631                    verify_finalized_transaction(
1632                        &included,
1633                        &intent,
1634                        nonce,
1635                        &prepared.raw_tx,
1636                        &executor,
1637                        trace_purpose,
1638                    )
1639                    .await?,
1640                );
1641
1642                if !finality_already_committed {
1643                    executor
1644                        .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
1645                        .await?;
1646                }
1647
1648                if let Some(plan) = plan
1649                    && plan.order.status() != OrderStatus::Rejected
1650                {
1651                    send_reverted_order(&self.emitter, &plan.order, &included)?;
1652                }
1653                database
1654                    .mark_execution_event_emitted(intent.id, "terminal")
1655                    .await?;
1656                executor.release_slot();
1657            }
1658            InclusionOutcome::Pending(message) => log::warn!("{message}"),
1659        }
1660        Ok(())
1661    }
1662
1663    /// Revalidates a restored wrap or approve against the finalized signer-nonce transaction
1664    /// and reruns the live postcondition before the restored intent may report success.
1665    ///
1666    /// Fails closed: any identity mismatch or unproven postcondition returns an error, which
1667    /// keeps the in-flight signer slot occupied, leaves the intent active, and fails connect.
1668    async fn verify_recovered_operator_transaction(
1669        &self,
1670        intent: &ExecutionIntentRow,
1671        purpose: TransactionPurpose,
1672        included: &IncludedTransaction,
1673        executor: &TransactionExecutor,
1674    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
1675        let (to, input, value) = persisted_call_fields(intent)?;
1676
1677        match purpose {
1678            TransactionPurpose::Wrap => {
1679                verify_wrap_balance_increase(executor, &to, value, included).await
1680            }
1681            TransactionPurpose::Approve => {
1682                let call = ERC20::approveCall::abi_decode(&input)
1683                    .with_context(|| "persisted approve calldata is invalid")?;
1684                verify_approve_allowance(executor, &to, &call.spender, call.amount, included).await
1685            }
1686            TransactionPurpose::Swap => {
1687                unreachable!("swap intents restore a swap plan")
1688            }
1689        }
1690    }
1691
1692    fn ensure_transaction_ready(&self, purpose: TransactionPurpose) -> anyhow::Result<()> {
1693        if !self.core.is_connected() {
1694            anyhow::bail!("Blockchain execution client is not connected");
1695        }
1696
1697        {
1698            let slot = self.in_flight.lock();
1699            if let Some(in_flight) = *slot {
1700                return Err(in_flight_limit_error(&in_flight));
1701            }
1702        }
1703
1704        if !self.cache.has_database() {
1705            anyhow::bail!(
1706                "No durable store configured; refusing to submit a {} transaction",
1707                purpose.as_str()
1708            );
1709        }
1710        Ok(())
1711    }
1712
1713    /// Validates a submit-order command against the configured policy and builds the swap plan.
1714    ///
1715    /// Local checks run first: pool resolution, order semantics, allowlists, amount and
1716    /// slippage limits, and the quote derived from the live pool profiler. Infrastructure
1717    /// readiness (connection, in-flight slot, durable store, signer) follows. Chain state is
1718    /// verified in the spawned task before signing.
1719    fn prepare_swap(&self, cmd: &SubmitOrder, order: &OrderAny) -> anyhow::Result<SwapPlan> {
1720        let instrument_id = order.instrument_id();
1721        let pool = self.resolve_pool(&instrument_id)?;
1722
1723        if order.order_type() != OrderType::Market {
1724            anyhow::bail!(
1725                "Unsupported order type {}; only Market is supported",
1726                order.order_type()
1727            );
1728        }
1729
1730        if !matches!(order.order_side(), OrderSide::Buy | OrderSide::Sell) {
1731            anyhow::bail!(
1732                "Unsupported order side {}; only Buy and Sell are supported",
1733                order.order_side()
1734            );
1735        }
1736
1737        if order.is_quote_quantity() {
1738            anyhow::bail!(
1739                "Quote-denominated quantities are not supported; quantity must be denominated in the base token"
1740            );
1741        }
1742
1743        let fee = pool
1744            .fee
1745            .ok_or_else(|| anyhow::anyhow!("Pool {instrument_id} has no fee tier"))?;
1746        let fee = U24::try_from(fee)
1747            .map_err(|_| anyhow::anyhow!("Pool {instrument_id} fee {fee} exceeds uint24"))?;
1748
1749        let base_token = pool.get_base_token();
1750        let quote_token = pool.get_quote_token();
1751        let quote_currency = Currency::new_checked(
1752            &quote_token.symbol,
1753            quote_token.decimals,
1754            0,
1755            &quote_token.name,
1756            CurrencyType::Crypto,
1757        )?;
1758        let (token_in, token_out) =
1759            swap_token_pair(order.order_side(), base_token.address, quote_token.address)?;
1760
1761        if !self
1762            .transaction_limits
1763            .allowed_token_pairs
1764            .contains(&(token_in, token_out))
1765        {
1766            anyhow::bail!(
1767                "Token pair {token_in} -> {token_out} is not in the `allowed_token_pairs` allowlist"
1768            );
1769        }
1770
1771        let base_amount = quantity_to_raw_amount(order.quantity(), base_token.decimals)?;
1772        if base_amount > U256::from(self.transaction_limits.max_order_amount) {
1773            anyhow::bail!(
1774                "Order amount {base_amount} exceeds the configured `max_order_amount` {}",
1775                self.transaction_limits.max_order_amount
1776            );
1777        }
1778
1779        let slippage_bps = match cmd
1780            .params
1781            .as_ref()
1782            .and_then(|params| params.get_u64("slippage_bps"))
1783        {
1784            Some(value) => u32::try_from(value).map_err(|_| {
1785                anyhow::anyhow!("slippage_bps parameter {value} exceeds the u32 range")
1786            })?,
1787            None => self.transaction_limits.slippage_bps,
1788        };
1789
1790        if slippage_bps > self.transaction_limits.max_slippage_bps {
1791            anyhow::bail!(
1792                "Slippage {slippage_bps} bps exceeds the configured `max_slippage_bps` {}",
1793                self.transaction_limits.max_slippage_bps
1794            );
1795        }
1796
1797        let quote_spend_ceiling = if order.order_side() == OrderSide::Buy {
1798            let ceiling = self
1799                .transaction_limits
1800                .quote_spend_limits
1801                .get(&(token_in, token_out))
1802                .ok_or_else(|| {
1803                    anyhow::anyhow!(
1804                        "No `quote_spend_limits` entry for BUY token pair {token_in} -> {token_out}"
1805                    )
1806                })?;
1807            anyhow::ensure!(
1808                ceiling.spend_token == quote_token.address,
1809                "Quote spend limit for {token_in} -> {token_out} is denominated in {}, expected quote token {}",
1810                ceiling.spend_token,
1811                quote_token.address
1812            );
1813            anyhow::ensure!(
1814                ceiling.spend_token_decimals == quote_token.decimals,
1815                "Quote spend limit for token {} uses {} decimals, expected pool quote-token decimals {}",
1816                ceiling.spend_token,
1817                ceiling.spend_token_decimals,
1818                quote_token.decimals
1819            );
1820            Some(ceiling)
1821        } else {
1822            None
1823        };
1824
1825        let profiler = self
1826            .core
1827            .cache()
1828            .pool_profiler(&instrument_id)
1829            .cloned()
1830            .ok_or_else(|| {
1831                anyhow::anyhow!(
1832                    "No pool profiler for {instrument_id}; an active data subscription is required to quote the swap"
1833                )
1834            })?;
1835
1836        if !profiler.is_initialized {
1837            anyhow::bail!("Pool profiler for {instrument_id} is not initialized");
1838        }
1839        let profiler_position = profiler.last_processed_event.clone().ok_or_else(|| {
1840            anyhow::anyhow!("Pool profiler for {instrument_id} has processed no events")
1841        })?;
1842
1843        let zero_for_one = token_in == pool.token0.address;
1844        let (amount_in, quoted_amount_out) = match order.order_side() {
1845            OrderSide::Sell => {
1846                let quote = profiler
1847                    .swap_exact_in(base_amount, zero_for_one, None)
1848                    .map_err(|e| anyhow::anyhow!("Swap quote failed for {instrument_id}: {e}"))?;
1849                let amount_filled = if zero_for_one {
1850                    quote.amount0
1851                } else {
1852                    quote.amount1
1853                };
1854
1855                if amount_filled != I256::from(base_amount) {
1856                    anyhow::bail!(
1857                        "Local quote for {instrument_id} filled {amount_filled} of the {base_amount} order amount; pool liquidity cannot fill the order"
1858                    );
1859                }
1860                (base_amount, exact_output_amount(&quote, zero_for_one)?)
1861            }
1862            OrderSide::Buy => {
1863                let quote = profiler
1864                    .swap_exact_out(base_amount, zero_for_one, None)
1865                    .map_err(|e| anyhow::anyhow!("Swap quote failed for {instrument_id}: {e}"))?;
1866                let amount_in = quote.get_input_amount();
1867                if amount_in.is_zero() {
1868                    anyhow::bail!("Local quote for {instrument_id} produced a zero quote input");
1869                }
1870                let ceiling = quote_spend_ceiling.ok_or_else(|| {
1871                    anyhow::anyhow!(
1872                        "No `quote_spend_limits` entry for BUY token pair {token_in} -> {token_out}"
1873                    )
1874                })?;
1875
1876                if amount_in > ceiling.max_amount {
1877                    anyhow::bail!(
1878                        "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits` maximum {} for {token_in} -> {token_out}",
1879                        ceiling.max_amount
1880                    );
1881                }
1882                (amount_in, base_amount)
1883            }
1884        };
1885        let min_amount_out = derive_min_amount_out(quoted_amount_out, slippage_bps)?;
1886
1887        self.ensure_transaction_ready(TransactionPurpose::Swap)?;
1888
1889        if self.signer.is_none() {
1890            anyhow::bail!("Signer not initialized; connect the client first");
1891        }
1892
1893        let pool_address = pool.address;
1894        let factory = self.uniswap_v3_factory()?;
1895        anyhow::ensure!(
1896            pool.dex.factory == factory,
1897            "Pool {instrument_id} references factory {}, expected registered factory {factory}",
1898            pool.dex.factory
1899        );
1900
1901        Ok(SwapPlan {
1902            order: order.clone(),
1903            pool,
1904            quote_currency,
1905            instrument_id,
1906            pool_address,
1907            router: self.router_addresses[0],
1908            factory,
1909            weth: self.weth_address,
1910            token_in,
1911            token_out,
1912            fee,
1913            amount_in,
1914            min_amount_out,
1915            slippage_bps,
1916            quote_spend_ceiling: quote_spend_ceiling.copied(),
1917            profiler_position: Some(profiler_position),
1918        })
1919    }
1920}
1921
1922/// A locally signed EIP-1559 transaction ready for persist-before-broadcast.
1923struct PreparedTransaction {
1924    intent_id: i64,
1925    created_block: u64,
1926    nonce: u64,
1927    tx_hash: B256,
1928    raw_tx: Vec<u8>,
1929    payload_lease: Option<ExecutionPayloadLease>,
1930}
1931
1932impl Debug for PreparedTransaction {
1933    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1934        f.debug_struct(stringify!(PreparedTransaction))
1935            .field("intent_id", &self.intent_id)
1936            .field("created_block", &self.created_block)
1937            .field("nonce", &self.nonce)
1938            .field("tx_hash", &self.tx_hash)
1939            .field("raw_tx", &"<redacted>")
1940            .field(
1941                "payload_lease",
1942                &self.payload_lease.as_ref().map(|_| "held"),
1943            )
1944            .finish()
1945    }
1946}
1947
1948/// The classified outcome of a broadcast attempt.
1949#[derive(Debug)]
1950enum BroadcastOutcome {
1951    /// The node accepted the transaction (or reported it already known).
1952    Accepted,
1953    /// Acceptance is uncertain; the persisted record stays active and the slot occupied.
1954    Ambiguous(String),
1955}
1956
1957/// The classified outcome of awaiting transaction finality.
1958#[derive(Debug)]
1959enum InclusionOutcome {
1960    /// Canonical successful receipt observed through a stable finalized boundary.
1961    Finalized(IncludedTransaction),
1962    /// Canonical failed receipt observed through a stable finalized boundary.
1963    Reverted(IncludedTransaction),
1964    /// No receipt arrived within the poll budget or observation failed; the record stays
1965    /// pending and the in-flight slot occupied.
1966    Pending(String),
1967}
1968
1969enum ReconciliationAuthorization {
1970    Rebroadcast,
1971    Retain,
1972    ScanReplacement(VerifiedBlockHeader),
1973}
1974
1975fn verified_value<T>(outcome: VerificationOutcome<T>, context: &str) -> anyhow::Result<T> {
1976    required_verification(outcome, context).map(|verified| verified.value)
1977}
1978
1979fn required_verification<T>(
1980    outcome: VerificationOutcome<T>,
1981    context: &str,
1982) -> anyhow::Result<Verified<T>> {
1983    match outcome {
1984        VerificationOutcome::Verified(verified) => Ok(verified),
1985        VerificationOutcome::Disagreement(_) => {
1986            anyhow::bail!("{context} verification disagreed")
1987        }
1988        VerificationOutcome::Unavailable(_) => {
1989            anyhow::bail!("{context} verification is unavailable")
1990        }
1991        VerificationOutcome::Retryable(_) => {
1992            anyhow::bail!("{context} verification is retryable")
1993        }
1994        VerificationOutcome::LocallyInvalid(_) => {
1995            anyhow::bail!("{context} verification is locally invalid")
1996        }
1997    }
1998}
1999
2000fn validate_transaction_authorization(
2001    authorization: Option<&TransactionAuthorization>,
2002    to: Address,
2003    value: U256,
2004    input: &[u8],
2005) -> anyhow::Result<()> {
2006    match authorization {
2007        None => Ok(()),
2008        Some(TransactionAuthorization::Wrap { weth }) => {
2009            anyhow::ensure!(
2010                to == *weth && !value.is_zero() && input == WETH9::depositCall::SELECTOR,
2011                "Wrap authorization does not match the transaction call"
2012            );
2013            Ok(())
2014        }
2015        Some(TransactionAuthorization::Approve {
2016            token,
2017            router,
2018            amount,
2019        }) => {
2020            let expected = ERC20::approveCall {
2021                spender: *router,
2022                amount: *amount,
2023            }
2024            .abi_encode();
2025            anyhow::ensure!(
2026                to == *token && value.is_zero() && input == expected,
2027                "Approve authorization does not match the transaction call"
2028            );
2029            Ok(())
2030        }
2031    }
2032}
2033
2034fn verification_decision<T>(
2035    verified: &Verified<T>,
2036    height_start: Option<u64>,
2037    height_end: Option<u64>,
2038) -> ExecutionVerificationDecision {
2039    ExecutionVerificationDecision {
2040        read_class: verified.read.as_str(),
2041        height_start,
2042        height_end,
2043        normalized_value_digest: verified.normalized_value_digest.to_string(),
2044    }
2045}
2046
2047fn parse_verified_header(header: &ExecutionVerifiedHeader) -> anyhow::Result<VerifiedBlockHeader> {
2048    Ok(VerifiedBlockHeader {
2049        number: header.number,
2050        hash: B256::from_str(&header.hash).context("Durable finalized header hash is invalid")?,
2051        parent_hash: B256::from_str(&header.parent_hash)
2052            .context("Durable finalized parent hash is invalid")?,
2053        timestamp: header.timestamp,
2054        base_fee_per_gas: header.base_fee_per_gas,
2055    })
2056}
2057
2058fn durable_verified_header(header: &VerifiedBlockHeader) -> ExecutionVerifiedHeader {
2059    ExecutionVerifiedHeader {
2060        number: header.number,
2061        hash: header.hash.to_string(),
2062        parent_hash: header.parent_hash.to_string(),
2063        timestamp: header.timestamp,
2064        base_fee_per_gas: header.base_fee_per_gas,
2065    }
2066}
2067
2068/// Shared execution context driving the EIP-1559 transaction pipeline.
2069///
2070/// Wrap, approve, and swap submissions share one implementation so the chain, fee, gas,
2071/// signing, persistence, and broadcast policy cannot drift between operator transactions
2072/// and order flow. Carries only `Send` state so order submission can run on a spawned task
2073/// after the synchronous trait method returns.
2074#[derive(Debug, Clone)]
2075struct TransactionExecutor {
2076    http_rpc_client: Arc<BlockchainHttpRpcClient>,
2077    verification: VerificationCoordinator,
2078    manifest_version: String,
2079    manifest_digest: String,
2080    deployment_manifest: Arc<BlockchainDeploymentManifest>,
2081    provider_ids: Vec<String>,
2082    operator_ids: Vec<String>,
2083    failure_domain_ids: Vec<String>,
2084    database: BlockchainCacheDatabase,
2085    signer: Arc<PrivateKeySigner>,
2086    payload_keys: Arc<PayloadKeySet>,
2087    in_flight: Arc<Mutex<Option<InFlightSlot>>>,
2088    wallet_balance: Arc<Mutex<WalletBalance>>,
2089    account_id: AccountId,
2090    wallet_address: Address,
2091    chain_id: u32,
2092    max_fee_per_gas_wei: u64,
2093    base_fee_buffer_bps: u32,
2094    gas_limit: u64,
2095    gas_buffer_bps: u32,
2096    receipt_timeout: Duration,
2097    receipt_max_polls: u32,
2098}
2099
2100impl TransactionExecutor {
2101    /// Builds, signs, persists, and broadcasts an EIP-1559 transaction, then awaits finality.
2102    ///
2103    /// Order of operations: claim the in-flight slot, chain ID verification, nonce and fee and
2104    /// gas policy checks, local signing, persist-before-broadcast, broadcast, inclusion
2105    /// observation. The persisted record guarantees a signed transaction is never forgotten.
2106    async fn transact(
2107        &self,
2108        to: Address,
2109        value: U256,
2110        input: Bytes,
2111        purpose: TransactionPurpose,
2112        client_order_id: Option<ClientOrderId>,
2113        authorization: TransactionAuthorization,
2114    ) -> anyhow::Result<IncludedTransaction> {
2115        self.claim_slot(purpose)?;
2116        let now_unix_secs = current_unix_secs()?;
2117        let decision_header = match required_verification(
2118            self.verification
2119                .verify_decision_header(now_unix_secs)
2120                .await,
2121            "operator decision header",
2122        ) {
2123            Ok(header) => header,
2124            Err(e) => {
2125                release_preparing_slot(&self.in_flight);
2126                return Err(e);
2127            }
2128        };
2129        let created_block = decision_header.value.number;
2130        let intent = ExecutionIntentInsert {
2131            chain_id: self.chain_id,
2132            wallet_address: self.wallet_address.to_string(),
2133            purpose: purpose.as_str().to_string(),
2134            client_order_id: client_order_id.map(|id| id.to_string()),
2135            trader_id: None,
2136            strategy_id: None,
2137            account_id: None,
2138            instrument_id: None,
2139            pool_address: None,
2140            transaction_to: to.to_string(),
2141            transaction_input: hex::encode_prefixed(&input),
2142            transaction_value: value.to_string(),
2143            amount_in: None,
2144            created_block,
2145        };
2146        let intent = match self.database.reserve_execution_intent(&intent).await {
2147            Ok(intent) => intent,
2148            Err(e) => {
2149                release_preparing_if_reservation_not_committed(&self.in_flight, &e);
2150                return Err(e);
2151            }
2152        };
2153        let prepared = match self
2154            .prepare_and_sign(
2155                intent.id,
2156                intent.created_block,
2157                to,
2158                value,
2159                input,
2160                &authorization,
2161                decision_header,
2162            )
2163            .await
2164        {
2165            Ok(prepared) => prepared,
2166            Err(e) => {
2167                if self
2168                    .database
2169                    .mark_execution_intent_recoverable(intent.id)
2170                    .await
2171                    .is_ok()
2172                {
2173                    release_preparing_slot(&self.in_flight);
2174                }
2175                return Err(e);
2176            }
2177        };
2178        self.fill_and_persist(&prepared, purpose).await?;
2179
2180        match self.broadcast(&prepared).await? {
2181            BroadcastOutcome::Accepted => {}
2182            BroadcastOutcome::Ambiguous(message) => log::warn!("{message}"),
2183        }
2184
2185        match self.await_finality(&prepared).await? {
2186            InclusionOutcome::Finalized(mut included) => {
2187                included.finality.decisions.extend(
2188                    verify_finalized_transaction(
2189                        &included,
2190                        &intent,
2191                        prepared.nonce,
2192                        &prepared.raw_tx,
2193                        self,
2194                        purpose.as_str(),
2195                    )
2196                    .await?,
2197                );
2198                Ok(included)
2199            }
2200            InclusionOutcome::Reverted(mut included) => {
2201                included.finality.decisions.extend(
2202                    verify_finalized_transaction(
2203                        &included,
2204                        &intent,
2205                        prepared.nonce,
2206                        &prepared.raw_tx,
2207                        self,
2208                        purpose.as_str(),
2209                    )
2210                    .await?,
2211                );
2212                self.commit_verified_finality(&included, TransactionStatus::Reverted, &[])
2213                    .await?;
2214                self.database
2215                    .mark_execution_event_emitted(prepared.intent_id, "terminal")
2216                    .await?;
2217                self.release_slot();
2218                anyhow::bail!("Transaction {} reverted on-chain", included.tx_hash)
2219            }
2220            InclusionOutcome::Pending(message) => anyhow::bail!(message),
2221        }
2222    }
2223
2224    /// Claims the single in-flight slot before any preparation RPC call, so the `pending`
2225    /// nonce read stays authoritative: a second transaction is rejected before it can sign.
2226    fn claim_slot(&self, purpose: TransactionPurpose) -> anyhow::Result<()> {
2227        let mut slot = self.in_flight.lock();
2228        if let Some(in_flight) = *slot {
2229            return Err(in_flight_limit_error(&in_flight));
2230        }
2231        *slot = Some(InFlightSlot::Preparing(purpose));
2232        Ok(())
2233    }
2234
2235    /// Runs the read-only pre-signing pipeline: chain ID verification, nonce selection, fee
2236    /// and gas policy checks, transaction building, and local signing.
2237    #[expect(
2238        clippy::too_many_arguments,
2239        reason = "Security-critical transaction fields stay explicit at the signing boundary"
2240    )]
2241    async fn prepare_and_sign(
2242        &self,
2243        intent_id: i64,
2244        created_block: u64,
2245        to: Address,
2246        value: U256,
2247        input: Bytes,
2248        authorization: &TransactionAuthorization,
2249        decision_header: Verified<VerifiedBlockHeader>,
2250    ) -> anyhow::Result<PreparedTransaction> {
2251        self.prepare_and_sign_with_anchors(
2252            intent_id,
2253            created_block,
2254            to,
2255            value,
2256            input,
2257            None,
2258            Some(authorization),
2259            Some(decision_header),
2260        )
2261        .await
2262    }
2263
2264    async fn prepare_and_sign_swap(
2265        &self,
2266        intent_id: i64,
2267        created_block: u64,
2268        to: Address,
2269        value: U256,
2270        input: Bytes,
2271        anchors: &SwapQuoteAnchors,
2272    ) -> anyhow::Result<PreparedTransaction> {
2273        self.prepare_and_sign_with_anchors(
2274            intent_id,
2275            created_block,
2276            to,
2277            value,
2278            input,
2279            Some(anchors),
2280            None,
2281            None,
2282        )
2283        .await
2284    }
2285
2286    #[expect(
2287        clippy::too_many_arguments,
2288        reason = "Security-critical transaction fields and verification anchors stay explicit"
2289    )]
2290    async fn prepare_and_sign_with_anchors(
2291        &self,
2292        intent_id: i64,
2293        created_block: u64,
2294        to: Address,
2295        value: U256,
2296        input: Bytes,
2297        swap_anchors: Option<&SwapQuoteAnchors>,
2298        authorization: Option<&TransactionAuthorization>,
2299        decision_header: Option<Verified<VerifiedBlockHeader>>,
2300    ) -> anyhow::Result<PreparedTransaction> {
2301        let expected_chain_id = u64::from(self.chain_id);
2302        let chain_id_verification = required_verification(
2303            self.verification.verify_chain_id().await,
2304            "pre-sign chain ID",
2305        )?;
2306        let actual_chain_id = chain_id_verification.value;
2307        anyhow::ensure!(
2308            actual_chain_id == expected_chain_id,
2309            "Verified chain ID does not match the transaction chain"
2310        );
2311        let decision_header_verification = if let Some(anchors) = swap_anchors {
2312            required_verification(
2313                self.verification.verify_block(anchors.state.number).await,
2314                "pre-sign swap decision header reread",
2315            )?
2316        } else {
2317            match decision_header {
2318                Some(verified) => verified,
2319                None => {
2320                    let now_unix_secs = current_unix_secs()?;
2321                    required_verification(
2322                        self.verification
2323                            .verify_decision_header(now_unix_secs)
2324                            .await,
2325                        "pre-sign decision header",
2326                    )?
2327                }
2328            }
2329        };
2330        let decision_header = decision_header_verification.value;
2331
2332        if let Some(anchors) = swap_anchors {
2333            anyhow::ensure!(
2334                decision_header == anchors.state,
2335                "Verified swap decision header changed before signing"
2336            );
2337        }
2338        let decision_ancestry = self.verify_decision_ancestry(decision_header).await?;
2339        validate_transaction_authorization(authorization, to, value, &input)?;
2340        let deployment_verification = required_verification(
2341            self.verification
2342                .verify_deployment_manifest(&self.deployment_manifest, decision_header.number)
2343                .await,
2344            "pre-sign deployment manifest",
2345        )?;
2346        let authorization_decisions = match authorization {
2347            Some(authorization) => {
2348                self.verify_transaction_authorization(authorization, decision_header.number)
2349                    .await?
2350            }
2351            None => Vec::new(),
2352        };
2353        let base_fee_per_gas_wei = decision_header.base_fee_per_gas.ok_or_else(|| {
2354            anyhow::anyhow!(
2355                "Verified decision block {} has no base fee",
2356                decision_header.number
2357            )
2358        })?;
2359        let priority_fee_verification = required_verification(
2360            self.verification.verify_priority_fee().await,
2361            "pre-sign priority fee",
2362        )?;
2363        let priority_fee_per_gas_wei = priority_fee_verification.value;
2364        let (max_fee_per_gas, max_priority_fee_per_gas) = derive_fees(
2365            base_fee_per_gas_wei,
2366            priority_fee_per_gas_wei,
2367            self.base_fee_buffer_bps,
2368            u128::from(self.max_fee_per_gas_wei),
2369        )?;
2370        let gas_estimate_verification = required_verification(
2371            self.verification
2372                .verify_gas_estimate(
2373                    &self.wallet_address,
2374                    &to,
2375                    value,
2376                    &input,
2377                    decision_header.number,
2378                )
2379                .await,
2380            "pre-sign gas estimate",
2381        )?;
2382        let gas_estimate = gas_estimate_verification.value;
2383        let gas_limit = derive_gas_limit(gas_estimate, self.gas_buffer_bps, self.gas_limit)?;
2384        let max_gas_cost = U256::from(gas_limit)
2385            .checked_mul(U256::from(max_fee_per_gas))
2386            .ok_or_else(|| anyhow::anyhow!("Maximum gas cost overflow"))?;
2387        let max_transaction_cost = value
2388            .checked_add(max_gas_cost)
2389            .ok_or_else(|| anyhow::anyhow!("Maximum transaction cost overflow"))?;
2390        let native_balance_verification = required_verification(
2391            self.verification
2392                .verify_balance(&self.wallet_address, decision_header.number)
2393                .await,
2394            "pre-sign native balance",
2395        )?;
2396        let native_balance = native_balance_verification.value;
2397
2398        if native_balance < max_transaction_cost {
2399            anyhow::bail!(
2400                "Native currency balance {native_balance} wei is below maximum transaction cost {max_transaction_cost} wei"
2401            );
2402        }
2403        let decision_height = Some(decision_header.number);
2404        let mut decisions = vec![
2405            verification_decision(&chain_id_verification, None, None),
2406            verification_decision(
2407                &decision_header_verification,
2408                decision_height,
2409                decision_height,
2410            ),
2411            verification_decision(&deployment_verification, decision_height, decision_height),
2412            verification_decision(&priority_fee_verification, None, None),
2413            verification_decision(&gas_estimate_verification, decision_height, decision_height),
2414            verification_decision(
2415                &native_balance_verification,
2416                decision_height,
2417                decision_height,
2418            ),
2419        ];
2420        decisions.extend(decision_ancestry);
2421        decisions.extend(authorization_decisions);
2422        if let Some(anchors) = swap_anchors {
2423            decisions.extend(self.verify_swap_anchors_before_sign(anchors).await?);
2424            decisions.extend(anchors.precondition_decisions.iter().cloned());
2425        } else {
2426            decisions.extend(self.verify_pre_sign_header_fence(decision_header).await?);
2427        }
2428        let canonical_nonce_verification = required_verification(
2429            self.verification
2430                .verify_transaction_count(&self.wallet_address, decision_header.number)
2431                .await,
2432            "pre-sign canonical nonce reread",
2433        )?;
2434        let pending_nonce_verification = required_verification(
2435            self.verification
2436                .verify_pending_transaction_count(&self.wallet_address)
2437                .await,
2438            "pre-sign pending nonce reread",
2439        )?;
2440        anyhow::ensure!(
2441            pending_nonce_verification.value == canonical_nonce_verification.value,
2442            "Pending nonce does not match the verified canonical nonce"
2443        );
2444        let nonce = canonical_nonce_verification.value;
2445        decisions.push(verification_decision(
2446            &canonical_nonce_verification,
2447            decision_height,
2448            decision_height,
2449        ));
2450        decisions.push(verification_decision(
2451            &pending_nonce_verification,
2452            None,
2453            None,
2454        ));
2455        let tx = build_eip1559_transaction(
2456            expected_chain_id,
2457            nonce,
2458            gas_limit,
2459            max_fee_per_gas,
2460            max_priority_fee_per_gas,
2461            to,
2462            value,
2463            input,
2464        );
2465        let wallet_address = self.wallet_address.to_string();
2466        self.database
2467            .assign_execution_intent_nonce_verified(&ExecutionNonceAssignment {
2468                intent_id,
2469                chain_id: self.chain_id,
2470                wallet_address: &wallet_address,
2471                nonce,
2472                manifest_version: &self.manifest_version,
2473                manifest_digest: &self.manifest_digest,
2474                provider_ids: &self.provider_ids,
2475                operator_ids: &self.operator_ids,
2476                failure_domain_ids: &self.failure_domain_ids,
2477                decisions: &decisions,
2478            })
2479            .await?;
2480        let payload_lease = self
2481            .database
2482            .acquire_execution_payload_lease(&self.payload_keys)
2483            .await?;
2484        let (tx_hash, raw_tx) = sign_eip1559_transaction(tx, &self.signer).await?;
2485
2486        Ok(PreparedTransaction {
2487            intent_id,
2488            created_block,
2489            nonce,
2490            tx_hash,
2491            raw_tx,
2492            payload_lease: Some(payload_lease),
2493        })
2494    }
2495
2496    async fn verify_pre_sign_header_fence(
2497        &self,
2498        target: VerifiedBlockHeader,
2499    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2500        let checkpoint = required_verification(
2501            self.verification.verify_checkpoint().await,
2502            "pre-sign checkpoint reread",
2503        )?;
2504        let header = required_verification(
2505            self.verification.verify_block(target.number).await,
2506            "pre-sign decision header reread",
2507        )?;
2508        anyhow::ensure!(
2509            header.value == target,
2510            "Decision header changed before signing"
2511        );
2512        Ok(vec![
2513            verification_decision(
2514                &checkpoint,
2515                Some(checkpoint.value.number),
2516                Some(checkpoint.value.number),
2517            ),
2518            verification_decision(&header, Some(target.number), Some(target.number)),
2519        ])
2520    }
2521
2522    async fn verify_decision_ancestry(
2523        &self,
2524        target: VerifiedBlockHeader,
2525    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2526        let checkpoint = required_verification(
2527            self.verification.verify_checkpoint().await,
2528            "pre-sign checkpoint",
2529        )?;
2530        anyhow::ensure!(
2531            checkpoint.value.number <= target.number,
2532            "Pre-sign decision header precedes the trusted checkpoint"
2533        );
2534        let mut decisions = vec![verification_decision(
2535            &checkpoint,
2536            Some(checkpoint.value.number),
2537            Some(checkpoint.value.number),
2538        )];
2539        let wallet_address = self.wallet_address.to_string();
2540        let position = self
2541            .database
2542            .load_execution_verification_position(
2543                self.chain_id,
2544                &wallet_address,
2545                &self.manifest_version,
2546                &self.manifest_digest,
2547            )
2548            .await?
2549            .ok_or_else(|| anyhow::anyhow!("Execution verification ledger is not initialized"))?;
2550        let durable_tip = parse_verified_header(&position.finalized_tip)?;
2551        anyhow::ensure!(
2552            durable_tip.number >= checkpoint.value.number && durable_tip.number <= target.number,
2553            "Pre-sign decision header does not extend the durable finalized header tip"
2554        );
2555        let durable_tip_verification = required_verification(
2556            self.verification.verify_block(durable_tip.number).await,
2557            "pre-sign durable finalized tip",
2558        )?;
2559        anyhow::ensure!(
2560            durable_tip_verification.value == durable_tip,
2561            "Durable finalized header tip conflicts with independent sources"
2562        );
2563
2564        if durable_tip != checkpoint.value {
2565            decisions.push(verification_decision(
2566                &durable_tip_verification,
2567                Some(durable_tip.number),
2568                Some(durable_tip.number),
2569            ));
2570        }
2571        let mut cursor = durable_tip;
2572        while cursor.number < target.number {
2573            let end = cursor.number.saturating_add(4_096).min(target.number);
2574            let start = cursor.number.saturating_add(1);
2575            let ancestry = required_verification(
2576                self.verification.verify_header_window(cursor, end).await,
2577                "pre-sign decision ancestry",
2578            )?;
2579            cursor = *ancestry
2580                .value
2581                .last()
2582                .expect("nonempty decision ancestry advances the cursor");
2583            decisions.push(verification_decision(&ancestry, Some(start), Some(end)));
2584        }
2585        anyhow::ensure!(
2586            cursor == target,
2587            "Pre-sign decision header conflicts with its trusted ancestry"
2588        );
2589        Ok(decisions)
2590    }
2591
2592    async fn verify_transaction_authorization(
2593        &self,
2594        authorization: &TransactionAuthorization,
2595        block: u64,
2596    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2597        match *authorization {
2598            TransactionAuthorization::Wrap { weth } => {
2599                let call = ERC20::balanceOfCall {
2600                    account: self.wallet_address,
2601                }
2602                .abi_encode();
2603                let balance = required_verification(
2604                    self.verification
2605                        .verify_decoded_call(None, &weth, U256::ZERO, &call, block, |result| {
2606                            ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into)
2607                        })
2608                        .await,
2609                    "pre-sign wrapped token probe",
2610                )?;
2611                Ok(vec![verification_decision(
2612                    &balance,
2613                    Some(block),
2614                    Some(block),
2615                )])
2616            }
2617            TransactionAuthorization::Approve {
2618                token,
2619                router,
2620                amount,
2621            } => {
2622                let allowance_call = ERC20::allowanceCall {
2623                    owner: self.wallet_address,
2624                    spender: router,
2625                }
2626                .abi_encode();
2627                let allowance = required_verification(
2628                    self.verification
2629                        .verify_decoded_call(
2630                            None,
2631                            &token,
2632                            U256::ZERO,
2633                            &allowance_call,
2634                            block,
2635                            |result| {
2636                                ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into)
2637                            },
2638                        )
2639                        .await,
2640                    "pre-sign router allowance",
2641                )?;
2642                anyhow::ensure!(
2643                    allowance.value.is_zero() || amount.is_zero(),
2644                    "Router allowance for token {token} is already {}; approve zero before setting a new nonzero allowance",
2645                    allowance.value
2646                );
2647
2648                let approve_call = ERC20::approveCall {
2649                    spender: router,
2650                    amount,
2651                }
2652                .abi_encode();
2653                let simulation = required_verification(
2654                    self.verification
2655                        .verify_decoded_simulation(
2656                            &self.wallet_address,
2657                            &token,
2658                            U256::ZERO,
2659                            &approve_call,
2660                            block,
2661                            |result| {
2662                                if result.is_empty() {
2663                                    Ok(true)
2664                                } else {
2665                                    ERC20::approveCall::abi_decode_returns_validate(result)
2666                                        .map_err(Into::into)
2667                                }
2668                            },
2669                        )
2670                        .await,
2671                    "pre-sign approval simulation",
2672                )?;
2673
2674                match &simulation.value {
2675                    VerifiedSimulation::Succeeded(true) => {}
2676                    VerifiedSimulation::Succeeded(false) => {
2677                        anyhow::bail!("ERC-20 approve returned false for token {token}")
2678                    }
2679                    VerifiedSimulation::Denied => {
2680                        anyhow::bail!("ERC-20 approve simulation reverted for token {token}")
2681                    }
2682                }
2683                Ok(vec![
2684                    verification_decision(&allowance, Some(block), Some(block)),
2685                    verification_decision(&simulation, Some(block), Some(block)),
2686                ])
2687            }
2688        }
2689    }
2690
2691    async fn verify_swap_anchors_before_sign(
2692        &self,
2693        anchors: &SwapQuoteAnchors,
2694    ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2695        let checkpoint = required_verification(
2696            self.verification.verify_checkpoint().await,
2697            "pre-sign checkpoint reread",
2698        )?;
2699        let watermark = required_verification(
2700            self.verification
2701                .verify_block(anchors.watermark.number)
2702                .await,
2703            "pre-sign profiler watermark reread",
2704        )?;
2705        anyhow::ensure!(
2706            watermark.value == anchors.watermark,
2707            "Pool state header changed before signing"
2708        );
2709        let ancestry = required_verification(
2710            self.verification
2711                .verify_header_window(watermark.value, anchors.state.number)
2712                .await,
2713            "pre-sign profiler ancestry reread",
2714        )?;
2715
2716        if let Some(last) = ancestry.value.last() {
2717            anyhow::ensure!(
2718                *last == anchors.state,
2719                "Swap decision header changed in the pre-sign ancestry reread"
2720            );
2721        } else {
2722            anyhow::ensure!(
2723                watermark.value == anchors.state,
2724                "Empty pre-sign ancestry does not end at the swap decision header"
2725            );
2726        }
2727        let quote = match anchors.quote_kind {
2728            SwapQuoteKind::ExactInput(amount_in) => required_verification(
2729                self.verification
2730                    .verify_quote_exact_input_single(
2731                        &anchors.quote_contract,
2732                        anchors.token_in,
2733                        anchors.token_out,
2734                        amount_in,
2735                        anchors.fee,
2736                        anchors.state.number,
2737                    )
2738                    .await,
2739                "pre-sign exact-input quote reread",
2740            )?,
2741            SwapQuoteKind::ExactOutput(amount_out) => required_verification(
2742                self.verification
2743                    .verify_quote_exact_output_single(
2744                        &anchors.quote_contract,
2745                        anchors.token_in,
2746                        anchors.token_out,
2747                        amount_out,
2748                        anchors.fee,
2749                        anchors.state.number,
2750                    )
2751                    .await,
2752                "pre-sign exact-output quote reread",
2753            )?,
2754        };
2755        anyhow::ensure!(
2756            quote.value == anchors.quote,
2757            "Independent swap quote changed before signing"
2758        );
2759        Ok(vec![
2760            verification_decision(
2761                &checkpoint,
2762                Some(checkpoint.value.number),
2763                Some(checkpoint.value.number),
2764            ),
2765            verification_decision(
2766                &watermark,
2767                Some(anchors.watermark.number),
2768                Some(anchors.watermark.number),
2769            ),
2770            verification_decision(
2771                &ancestry,
2772                Some(anchors.watermark.number),
2773                Some(anchors.state.number),
2774            ),
2775            verification_decision(
2776                &quote,
2777                Some(anchors.state.number),
2778                Some(anchors.state.number),
2779            ),
2780        ])
2781    }
2782
2783    /// Fills the claimed slot with the signed transaction and persists the pending record
2784    /// before broadcast.
2785    ///
2786    /// The slot is filled before the cancellable write: PostgreSQL may commit before this
2787    /// future resumes, so cancellation cannot safely restore the pre-transaction state. A
2788    /// persistence failure leaves the slot occupied because commit acknowledgement is
2789    /// ambiguous.
2790    async fn fill_and_persist(
2791        &self,
2792        prepared: &PreparedTransaction,
2793        purpose: TransactionPurpose,
2794    ) -> anyhow::Result<()> {
2795        {
2796            let mut slot = self.in_flight.lock();
2797            *slot = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
2798                intent_id: prepared.intent_id,
2799                nonce: prepared.nonce,
2800                tx_hash: prepared.tx_hash,
2801                purpose,
2802            }));
2803        }
2804
2805        let tx_hash = prepared.tx_hash;
2806        let transaction_hash = tx_hash.to_string();
2807        let policy = self.payload_policy();
2808        let intent = self
2809            .database
2810            .get_execution_intent(prepared.intent_id)
2811            .await?;
2812        authenticate_payload_identity(
2813            &prepared.raw_tx,
2814            &intent,
2815            &transaction_hash,
2816            self.chain_id,
2817            policy,
2818        )
2819        .with_context(|| format!("Newly signed transaction {tx_hash} failed authentication"))?;
2820
2821        self.database
2822            .reserve_execution_payload_seal(&self.payload_keys)
2823            .await?;
2824        let context = payload_context_identity(
2825            &intent,
2826            &transaction_hash,
2827            self.chain_id,
2828            self.payload_keys.deployment_id(),
2829        )?;
2830        let envelope = self.payload_keys.seal(&prepared.raw_tx, &context)?;
2831        let row = self
2832            .database
2833            .add_execution_transaction_envelope(
2834                prepared.intent_id,
2835                self.chain_id,
2836                &transaction_hash,
2837                &envelope,
2838            )
2839            .await
2840        .map_err(|e| {
2841            anyhow::anyhow!(
2842                "Failed to persist transaction {tx_hash}: {e}; the in-flight slot stays occupied"
2843            )
2844        })?;
2845        let stored = open_execution_payload(
2846            &self.payload_keys,
2847            policy,
2848            &intent,
2849            &row,
2850            "initial persistence",
2851        )?;
2852        anyhow::ensure!(
2853            stored == prepared.raw_tx,
2854            "Persisted transaction {tx_hash} does not match the signed bytes"
2855        );
2856        Ok(())
2857    }
2858
2859    fn payload_policy(&self) -> PayloadPolicy {
2860        PayloadPolicy {
2861            chain_id: self.chain_id,
2862            signer: self.wallet_address,
2863            gas_limit: self.gas_limit,
2864            max_fee_per_gas: self.max_fee_per_gas_wei,
2865        }
2866    }
2867
2868    async fn authorize_rebroadcast(
2869        &self,
2870        prepared: &PreparedTransaction,
2871        intent: &ExecutionIntentRow,
2872        purpose: TransactionPurpose,
2873    ) -> anyhow::Result<ReconciliationAuthorization> {
2874        let now_unix_secs = current_unix_secs()?;
2875        let decision_header = required_verification(
2876            self.verification
2877                .verify_decision_header(now_unix_secs)
2878                .await,
2879            "rebroadcast decision header",
2880        )?;
2881        let block = decision_header.value.number;
2882        let mut decisions = vec![verification_decision(
2883            &decision_header,
2884            Some(block),
2885            Some(block),
2886        )];
2887        decisions.extend(self.verify_decision_ancestry(decision_header.value).await?);
2888        let deployment = required_verification(
2889            self.verification
2890                .verify_deployment_manifest(&self.deployment_manifest, block)
2891                .await,
2892            "rebroadcast deployment manifest",
2893        )?;
2894        decisions.push(verification_decision(&deployment, Some(block), Some(block)));
2895        let canonical_nonce = required_verification(
2896            self.verification
2897                .verify_transaction_count(&self.wallet_address, block)
2898                .await,
2899            "rebroadcast canonical nonce",
2900        )?;
2901        decisions.push(verification_decision(
2902            &canonical_nonce,
2903            Some(block),
2904            Some(block),
2905        ));
2906        let pending_nonce = required_verification(
2907            self.verification
2908                .verify_reconciliation_pending_transaction_count(
2909                    &self.wallet_address,
2910                    prepared.nonce,
2911                )
2912                .await,
2913            "rebroadcast pending nonce",
2914        )?;
2915        decisions.push(verification_decision(&pending_nonce, None, None));
2916        let receipt_absence = required_verification(
2917            self.verification
2918                .verify_receipt_absence(&prepared.tx_hash)
2919                .await,
2920            "rebroadcast receipt absence",
2921        )?;
2922        decisions.push(verification_decision(&receipt_absence, None, None));
2923
2924        let next_nonce = prepared
2925            .nonce
2926            .checked_add(1)
2927            .ok_or_else(|| anyhow::anyhow!("Owned signer nonce overflow"))?;
2928        anyhow::ensure!(
2929            (prepared.nonce..=next_nonce).contains(&pending_nonce.value),
2930            "Pending nonce {} is outside the owned reconciliation range {}..={next_nonce}",
2931            pending_nonce.value,
2932            prepared.nonce
2933        );
2934        anyhow::ensure!(
2935            (prepared.nonce..=next_nonce).contains(&canonical_nonce.value),
2936            "Canonical nonce {} is outside the owned reconciliation range {}..={next_nonce}",
2937            canonical_nonce.value,
2938            prepared.nonce
2939        );
2940
2941        if !receipt_absence.value {
2942            self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
2943                .await?;
2944            return Ok(ReconciliationAuthorization::Retain);
2945        }
2946
2947        if canonical_nonce.value == next_nonce {
2948            self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
2949                .await?;
2950            return Ok(ReconciliationAuthorization::ScanReplacement(
2951                decision_header.value,
2952            ));
2953        }
2954
2955        let (to, input, value) = persisted_call_fields(intent)?;
2956        let authorized = match purpose {
2957            TransactionPurpose::Wrap => {
2958                let simulation = required_verification(
2959                    self.verification
2960                        .verify_decoded_simulation(
2961                            &self.wallet_address,
2962                            &to,
2963                            value,
2964                            &input,
2965                            block,
2966                            |result| Ok(result.is_empty()),
2967                        )
2968                        .await,
2969                    "rebroadcast wrap simulation",
2970                )?;
2971                let authorized = matches!(&simulation.value, VerifiedSimulation::Succeeded(true));
2972                decisions.push(verification_decision(&simulation, Some(block), Some(block)));
2973                authorized
2974            }
2975            TransactionPurpose::Approve => {
2976                let simulation = required_verification(
2977                    self.verification
2978                        .verify_decoded_simulation(
2979                            &self.wallet_address,
2980                            &to,
2981                            value,
2982                            &input,
2983                            block,
2984                            |result| {
2985                                if result.is_empty() {
2986                                    Ok(true)
2987                                } else {
2988                                    ERC20::approveCall::abi_decode_returns_validate(result)
2989                                        .map_err(Into::into)
2990                                }
2991                            },
2992                        )
2993                        .await,
2994                    "rebroadcast approve simulation",
2995                )?;
2996                let authorized = matches!(&simulation.value, VerifiedSimulation::Succeeded(true));
2997                decisions.push(verification_decision(&simulation, Some(block), Some(block)));
2998                authorized
2999            }
3000            TransactionPurpose::Swap => {
3001                let call = UniswapV3SwapRouter::exactInputSingleCall::abi_decode(&input)
3002                    .with_context(|| "persisted swap calldata is invalid")?;
3003
3004                if U256::from(decision_header.value.timestamp) > call.params.deadline {
3005                    false
3006                } else {
3007                    let simulation = required_verification(
3008                        self.verification
3009                            .verify_decoded_simulation(
3010                                &self.wallet_address,
3011                                &to,
3012                                value,
3013                                &input,
3014                                block,
3015                                |result| {
3016                                    UniswapV3SwapRouter::exactInputSingleCall::abi_decode_returns(
3017                                        result,
3018                                    )
3019                                    .map_err(Into::into)
3020                                },
3021                            )
3022                            .await,
3023                        "rebroadcast swap simulation",
3024                    )?;
3025                    let authorized = match &simulation.value {
3026                        VerifiedSimulation::Succeeded(amount_out) => {
3027                            *amount_out >= call.params.amountOutMinimum
3028                        }
3029                        VerifiedSimulation::Denied => false,
3030                    };
3031                    decisions.push(verification_decision(&simulation, Some(block), Some(block)));
3032                    authorized
3033                }
3034            }
3035        };
3036        self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
3037            .await?;
3038        Ok(if authorized {
3039            ReconciliationAuthorization::Rebroadcast
3040        } else {
3041            ReconciliationAuthorization::Retain
3042        })
3043    }
3044
3045    async fn scan_canonical_replacement(
3046        &self,
3047        intent: &ExecutionIntentRow,
3048        nonce: u64,
3049        head: VerifiedBlockHeader,
3050        authenticated_payloads: &HashMap<B256, Vec<u8>>,
3051    ) -> anyhow::Result<Option<(B256, Vec<u8>)>> {
3052        let wallet_address = self.wallet_address.to_string();
3053        let cursor = self
3054            .database
3055            .load_execution_replacement_cursor(
3056                intent.id,
3057                self.chain_id,
3058                &wallet_address,
3059                nonce,
3060                &self.manifest_digest,
3061            )
3062            .await?;
3063        let start = cursor.as_ref().map_or(intent.created_block, |header| {
3064            header.number.saturating_add(1)
3065        });
3066        anyhow::ensure!(
3067            start <= head.number,
3068            "Canonical nonce advanced without an authenticated signer transaction in the scanned canonical range"
3069        );
3070        let scan_range = replacement_scan_range(start, head.number)?;
3071        let end = *scan_range.end();
3072        let mut decisions = Vec::new();
3073        let mut blocks = Vec::new();
3074
3075        if let Some(cursor) = cursor.as_ref() {
3076            let parent = parse_verified_header(cursor)?;
3077            let window = required_verification(
3078                self.verification
3079                    .verify_replacement_window(parent, end)
3080                    .await,
3081                "canonical replacement window",
3082            )?;
3083            decisions.push(verification_decision(&window, Some(start), Some(end)));
3084            blocks = window.value;
3085        } else {
3086            let start_header = required_verification(
3087                self.verification.verify_block(start).await,
3088                "canonical replacement start header",
3089            )?;
3090            decisions.push(verification_decision(
3091                &start_header,
3092                Some(start),
3093                Some(start),
3094            ));
3095            let start_block = required_verification(
3096                self.verification.verify_replacement_block(start).await,
3097                "canonical replacement start block",
3098            )?;
3099            anyhow::ensure!(
3100                VerifiedBlockHeader::from(start_block.value.clone()) == start_header.value,
3101                "Replacement block conflicts with its canonical header"
3102            );
3103            decisions.push(verification_decision(
3104                &start_block,
3105                Some(start),
3106                Some(start),
3107            ));
3108            blocks.push(start_block.value);
3109
3110            if end > start {
3111                let window = required_verification(
3112                    self.verification
3113                        .verify_replacement_window(start_header.value, end)
3114                        .await,
3115                    "canonical replacement window",
3116                )?;
3117                decisions.push(verification_decision(&window, Some(start + 1), Some(end)));
3118                blocks.extend(window.value);
3119            }
3120        }
3121
3122        let scanned_tip = blocks
3123            .last()
3124            .map(|block| VerifiedBlockHeader::from(block.clone()))
3125            .ok_or_else(|| anyhow::anyhow!("Verified replacement scan returned no blocks"))?;
3126        if end == head.number {
3127            anyhow::ensure!(
3128                scanned_tip == head,
3129                "Replacement scan tip conflicts with the verified canonical head"
3130            );
3131        }
3132        let mut candidates = blocks
3133            .iter()
3134            .flat_map(|block| block.transactions.iter())
3135            .filter(|transaction| {
3136                transaction.from == self.wallet_address && transaction.nonce == nonce
3137            });
3138        let candidate = candidates.next();
3139        anyhow::ensure!(
3140            candidates.next().is_none(),
3141            "Canonical replacement scan found duplicate signer-nonce transactions"
3142        );
3143
3144        let finalized_cursor = self
3145            .database
3146            .load_execution_verified_header(
3147                self.chain_id,
3148                &wallet_address,
3149                end,
3150                &self.manifest_digest,
3151            )
3152            .await?;
3153
3154        if let Some(cursor) = finalized_cursor.as_ref() {
3155            anyhow::ensure!(
3156                parse_verified_header(cursor)? == scanned_tip,
3157                "Replacement scan conflicts with the durable finalized header ledger"
3158            );
3159        }
3160
3161        let mut mismatch = None;
3162        let matched = candidate.and_then(|transaction| {
3163            let Some(raw_transaction) = authenticated_payloads.get(&transaction.hash).cloned()
3164            else {
3165                mismatch = Some(anyhow::anyhow!(
3166                    "Canonical signer-nonce transaction {} has no authenticated retained payload",
3167                    transaction.hash
3168                ));
3169                return None;
3170            };
3171
3172            if let Err(e) = validate_rpc_transaction_matches_payload(transaction, &raw_transaction)
3173            {
3174                mismatch = Some(e.context(format!(
3175                    "Canonical signer-nonce transaction {} failed authenticated payload validation",
3176                    transaction.hash
3177                )));
3178                return None;
3179            }
3180            Some((transaction.hash, raw_transaction))
3181        });
3182        let matched_hash = matched.as_ref().map(|(hash, _)| hash.to_string());
3183        self.database
3184            .record_execution_replacement_scan(&ExecutionReplacementScan {
3185                intent_id: intent.id,
3186                chain_id: self.chain_id,
3187                wallet_address: &wallet_address,
3188                nonce,
3189                finalized_cursor: finalized_cursor.as_ref(),
3190                matched_transaction_hash: matched_hash.as_deref(),
3191                manifest_version: &self.manifest_version,
3192                manifest_digest: &self.manifest_digest,
3193                provider_ids: &self.provider_ids,
3194                operator_ids: &self.operator_ids,
3195                failure_domain_ids: &self.failure_domain_ids,
3196                decisions: &decisions,
3197            })
3198            .await?;
3199
3200        if let Some(e) = mismatch {
3201            return Err(e);
3202        }
3203
3204        if matched.is_none() && end == head.number {
3205            anyhow::bail!(
3206                "Canonical nonce advanced without an authenticated signer transaction in the canonical range"
3207            );
3208        }
3209        Ok(matched)
3210    }
3211
3212    async fn persist_rebroadcast_decisions(
3213        &self,
3214        intent_id: i64,
3215        nonce: u64,
3216        decisions: &[ExecutionVerificationDecision],
3217    ) -> anyhow::Result<()> {
3218        let wallet_address = self.wallet_address.to_string();
3219        self.database
3220            .record_execution_verification_batch(&ExecutionVerificationBatch {
3221                intent_id,
3222                chain_id: self.chain_id,
3223                wallet_address: &wallet_address,
3224                nonce,
3225                decision_class: "rebroadcast",
3226                manifest_version: &self.manifest_version,
3227                manifest_digest: &self.manifest_digest,
3228                provider_ids: &self.provider_ids,
3229                operator_ids: &self.operator_ids,
3230                failure_domain_ids: &self.failure_domain_ids,
3231                decisions,
3232            })
3233            .await
3234    }
3235
3236    /// Broadcasts the signed transaction and classifies the acceptance outcome.
3237    async fn broadcast(&self, prepared: &PreparedTransaction) -> anyhow::Result<BroadcastOutcome> {
3238        let tx_hash = prepared.tx_hash;
3239
3240        self.database
3241            .record_execution_status(
3242                prepared.intent_id,
3243                &tx_hash.to_string(),
3244                TransactionStatus::Broadcast,
3245                None,
3246                None,
3247                None,
3248                None,
3249                None,
3250            )
3251            .await
3252            .map_err(|e| {
3253                anyhow::anyhow!(
3254                    "Failed to persist broadcast attempt for transaction {tx_hash}: {e}; the in-flight slot stays occupied"
3255                )
3256            })?;
3257
3258        match self
3259            .http_rpc_client
3260            .send_raw_transaction(&prepared.raw_tx, &tx_hash)
3261            .await
3262        {
3263            Ok(broadcast_hash) => {
3264                if broadcast_hash != tx_hash {
3265                    // The node acknowledged a different hash: acceptance of the signed
3266                    // transaction is unverified, so reconcile through the persisted record
3267                    // rather than poll a hash that cannot match the chain
3268                    return Ok(BroadcastOutcome::Ambiguous(format!(
3269                        "Broadcast of transaction {tx_hash} returned a differing hash {broadcast_hash}; the persisted record reconciles instead of rebroadcasting"
3270                    )));
3271                }
3272                Ok(BroadcastOutcome::Accepted)
3273            }
3274            Err(BroadcastError::TimeoutAfterSend) => Ok(BroadcastOutcome::Ambiguous(format!(
3275                "Broadcast of transaction {tx_hash} timed out after send; the persisted record reconciles instead of rebroadcasting"
3276            ))),
3277            Err(BroadcastError::Failed(message)) => {
3278                // Transport or response failure: acceptance is ambiguous (the failure may also
3279                // predate dispatch; see BroadcastError::Failed), so occupy the slot and
3280                // reconcile rather than risk a rebroadcast
3281                Ok(BroadcastOutcome::Ambiguous(format!(
3282                    "Broadcast of transaction {tx_hash} failed ambiguously ({message}); the persisted record reconciles instead of rebroadcasting"
3283                )))
3284            }
3285            Err(error @ BroadcastError::Rejected { .. }) => {
3286                Ok(BroadcastOutcome::Ambiguous(format!(
3287                    "Broadcast of transaction {tx_hash} was rejected ({error}); the signed hash remains occupied until canonical nonce reconciliation"
3288                )))
3289            }
3290        }
3291    }
3292
3293    /// Polls until the receipt is canonical at a stable finalized boundary.
3294    async fn await_finality(
3295        &self,
3296        prepared: &PreparedTransaction,
3297    ) -> anyhow::Result<InclusionOutcome> {
3298        let tx_hash = prepared.tx_hash;
3299        let deadline = tokio::time::Instant::now() + self.receipt_timeout;
3300
3301        for attempt in 0..self.receipt_max_polls {
3302            if tokio::time::Instant::now() >= deadline {
3303                break;
3304            }
3305
3306            if attempt > 0 {
3307                tokio::time::sleep(RECEIPT_POLL_INTERVAL).await;
3308            }
3309
3310            let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
3311            let receipt_result =
3312                match tokio::time::timeout(remaining, self.verification.verify_receipt(&tx_hash))
3313                    .await
3314                {
3315                    Ok(result) => result,
3316                    Err(_) => break,
3317                };
3318
3319            match receipt_result {
3320                VerificationOutcome::Verified(verified_receipt) => {
3321                    let receipt = &verified_receipt.value;
3322                    let canonical_verification = required_verification(
3323                        self.verification.verify_block(receipt.block_number).await,
3324                        "receipt inclusion header",
3325                    )?;
3326                    let canonical = canonical_verification.value;
3327
3328                    if canonical.hash != receipt.block_hash {
3329                        continue;
3330                    }
3331
3332                    if let Some(mut finality) = self.receipt_is_stably_finalized(receipt).await? {
3333                        finality.decisions.insert(
3334                            0,
3335                            verification_decision(
3336                                &canonical_verification,
3337                                Some(receipt.block_number),
3338                                Some(receipt.block_number),
3339                            ),
3340                        );
3341                        finality.decisions.insert(
3342                            0,
3343                            verification_decision(
3344                                &verified_receipt,
3345                                Some(receipt.block_number),
3346                                Some(receipt.block_number),
3347                            ),
3348                        );
3349                        let included = IncludedTransaction {
3350                            intent_id: prepared.intent_id,
3351                            nonce: prepared.nonce,
3352                            tx_hash,
3353                            block_number: receipt.block_number,
3354                            receipt: receipt.clone(),
3355                            finality,
3356                        };
3357                        return if receipt.status {
3358                            Ok(InclusionOutcome::Finalized(included))
3359                        } else {
3360                            Ok(InclusionOutcome::Reverted(included))
3361                        };
3362                    }
3363                }
3364                VerificationOutcome::Retryable(_) => {
3365                    continue;
3366                }
3367                VerificationOutcome::Disagreement(_) => {
3368                    return Ok(InclusionOutcome::Pending(format!(
3369                        "Receipt verification disagreed for transaction {tx_hash}; the intent stays occupied for reconciliation"
3370                    )));
3371                }
3372                VerificationOutcome::Unavailable(_) => {
3373                    log::warn!(
3374                        "Finality poll {}/{} for transaction {tx_hash} was unavailable",
3375                        attempt + 1,
3376                        self.receipt_max_polls
3377                    );
3378                }
3379                VerificationOutcome::LocallyInvalid(_) => {
3380                    anyhow::bail!(
3381                        "Receipt verification is locally invalid for transaction {tx_hash}"
3382                    );
3383                }
3384            }
3385        }
3386
3387        self.database
3388            .record_execution_status(
3389                prepared.intent_id,
3390                &tx_hash.to_string(),
3391                TransactionStatus::Dropped,
3392                None,
3393                None,
3394                None,
3395                None,
3396                None,
3397            )
3398            .await?;
3399        Ok(InclusionOutcome::Pending(format!(
3400            "Timed out awaiting finality of transaction {tx_hash}; the intent stays occupied for reconciliation"
3401        )))
3402    }
3403
3404    async fn receipt_is_stably_finalized(
3405        &self,
3406        receipt: &RpcTransactionReceipt,
3407    ) -> anyhow::Result<Option<StableFinality>> {
3408        let finalized_verification = match self.verification.verify_finalized_header().await {
3409            VerificationOutcome::Verified(verified) => verified,
3410            VerificationOutcome::Retryable(_) | VerificationOutcome::Unavailable(_) => {
3411                return Ok(None);
3412            }
3413            VerificationOutcome::Disagreement(_) => {
3414                anyhow::bail!("Finalized header verification disagreed")
3415            }
3416            VerificationOutcome::LocallyInvalid(_) => {
3417                anyhow::bail!("Finalized header verification is locally invalid")
3418            }
3419        };
3420        let finalized = finalized_verification.value;
3421        if finalized.number < receipt.block_number {
3422            return Ok(None);
3423        }
3424
3425        let checkpoint_verification = required_verification(
3426            self.verification.verify_checkpoint().await,
3427            "finality checkpoint reread",
3428        )?;
3429        let checkpoint = checkpoint_verification.value;
3430        let mut decisions = vec![verification_decision(
3431            &checkpoint_verification,
3432            Some(checkpoint.number),
3433            Some(checkpoint.number),
3434        )];
3435        let position = self
3436            .database
3437            .load_execution_verification_position(
3438                self.chain_id,
3439                &self.wallet_address.to_string(),
3440                &self.manifest_version,
3441                &self.manifest_digest,
3442            )
3443            .await?
3444            .ok_or_else(|| anyhow::anyhow!("Execution verification ledger is not initialized"))?;
3445        let durable_tip = parse_verified_header(&position.finalized_tip)?;
3446        anyhow::ensure!(
3447            durable_tip.number >= checkpoint.number,
3448            "Durable finalized header tip precedes the trusted checkpoint"
3449        );
3450        let mut finalized_headers = vec![durable_tip];
3451        let durable_tip_verification = required_verification(
3452            self.verification.verify_block(durable_tip.number).await,
3453            "finality durable header tip",
3454        )?;
3455        anyhow::ensure!(
3456            durable_tip_verification.value == durable_tip,
3457            "Durable finalized header tip conflicts with independent sources"
3458        );
3459        decisions.push(verification_decision(
3460            &durable_tip_verification,
3461            Some(durable_tip.number),
3462            Some(durable_tip.number),
3463        ));
3464        anyhow::ensure!(
3465            finalized.number >= durable_tip.number,
3466            "Verified finalized height regressed below the durable finalized header tip"
3467        );
3468        let mut ancestry_cursor = durable_tip;
3469        while ancestry_cursor.number < finalized.number {
3470            let end = ancestry_cursor
3471                .number
3472                .saturating_add(4_096)
3473                .min(finalized.number);
3474            let start = ancestry_cursor.number.saturating_add(1);
3475            let ancestry_verification = required_verification(
3476                self.verification
3477                    .verify_header_window(ancestry_cursor, end)
3478                    .await,
3479                "finality ancestry",
3480            )?;
3481            let ancestry = &ancestry_verification.value;
3482            ancestry_cursor = *ancestry
3483                .last()
3484                .expect("nonempty finality ancestry advances the cursor");
3485            decisions.push(verification_decision(
3486                &ancestry_verification,
3487                Some(start),
3488                Some(end),
3489            ));
3490            finalized_headers.extend(ancestry.iter().copied());
3491        }
3492        anyhow::ensure!(
3493            ancestry_cursor == finalized,
3494            "Finalized header conflicts with its verified ancestry"
3495        );
3496
3497        let canonical_again_verification = required_verification(
3498            self.verification.verify_block(receipt.block_number).await,
3499            "finality inclusion header reread",
3500        )?;
3501        let canonical_again = canonical_again_verification.value;
3502        let finalized_again_verification = required_verification(
3503            self.verification.verify_block(finalized.number).await,
3504            "finalized header reread",
3505        )?;
3506        let finalized_again = finalized_again_verification.value;
3507        anyhow::ensure!(
3508            canonical_again.hash == receipt.block_hash && finalized_again == finalized,
3509            "Finality verification disagreed with the receipt or finalized header"
3510        );
3511        decisions.extend([
3512            verification_decision(
3513                &finalized_verification,
3514                Some(finalized.number),
3515                Some(finalized.number),
3516            ),
3517            verification_decision(
3518                &canonical_again_verification,
3519                Some(receipt.block_number),
3520                Some(receipt.block_number),
3521            ),
3522            verification_decision(
3523                &finalized_again_verification,
3524                Some(finalized.number),
3525                Some(finalized.number),
3526            ),
3527        ]);
3528        Ok(Some(StableFinality {
3529            decisions,
3530            inclusion_header: durable_verified_header(&canonical_again),
3531            finalized_headers: finalized_headers
3532                .iter()
3533                .map(durable_verified_header)
3534                .collect(),
3535        }))
3536    }
3537
3538    async fn commit_verified_finality(
3539        &self,
3540        included: &IncludedTransaction,
3541        status: TransactionStatus,
3542        verified_postconditions: &[ExecutionVerificationDecision],
3543    ) -> anyhow::Result<()> {
3544        anyhow::ensure!(
3545            matches!(
3546                status,
3547                TransactionStatus::Finalized | TransactionStatus::Reverted
3548            ) && included.receipt.status == (status == TransactionStatus::Finalized),
3549            "Finality status conflicts with the verified transaction receipt"
3550        );
3551        let mut decisions = included.finality.decisions.clone();
3552        decisions.extend_from_slice(verified_postconditions);
3553        let wallet_address = self.wallet_address.to_string();
3554        let transaction_hash = included.tx_hash.to_string();
3555        let block_hash = included.receipt.block_hash.to_string();
3556        let effective_gas_price = included.receipt.effective_gas_price.to_string();
3557        self.database
3558            .record_execution_finality_verified(&ExecutionFinalityTransition {
3559                intent_id: included.intent_id,
3560                chain_id: self.chain_id,
3561                wallet_address: &wallet_address,
3562                nonce: included.nonce,
3563                transaction_hash: &transaction_hash,
3564                status,
3565                block_number: included.block_number,
3566                block_hash: &block_hash,
3567                receipt_success: included.receipt.status,
3568                gas_used: included.receipt.gas_used,
3569                effective_gas_price: &effective_gas_price,
3570                manifest_version: &self.manifest_version,
3571                manifest_digest: &self.manifest_digest,
3572                provider_ids: &self.provider_ids,
3573                operator_ids: &self.operator_ids,
3574                failure_domain_ids: &self.failure_domain_ids,
3575                decisions: &decisions,
3576                finalized_headers: &included.finality.finalized_headers,
3577            })
3578            .await
3579    }
3580
3581    fn release_slot(&self) {
3582        *self.in_flight.lock() = None;
3583    }
3584}
3585
3586fn replacement_scan_range(from_block: u64, head_block: u64) -> anyhow::Result<RangeInclusive<u64>> {
3587    anyhow::ensure!(
3588        head_block >= from_block,
3589        "Canonical head {head_block} is behind execution creation block {from_block}"
3590    );
3591    let max_end = from_block.saturating_add(MAX_REPLACEMENT_SCAN_BLOCKS - 1);
3592    Ok(from_block..=head_block.min(max_end))
3593}
3594
3595fn current_unix_secs() -> anyhow::Result<u64> {
3596    SystemTime::now()
3597        .duration_since(UNIX_EPOCH)
3598        .map_err(|_| anyhow::anyhow!("Trusted host clock precedes the Unix epoch"))
3599        .map(|duration| duration.as_secs())
3600}
3601
3602fn validate_payload_operation_batch_size(batch_size: usize) -> anyhow::Result<i64> {
3603    anyhow::ensure!(
3604        (1..=MAX_PAYLOAD_OPERATION_BATCH_SIZE).contains(&batch_size),
3605        "Payload operation batch size must be between 1 and {MAX_PAYLOAD_OPERATION_BATCH_SIZE}"
3606    );
3607    Ok(i64::try_from(batch_size).expect("bounded payload batch size fits i64"))
3608}
3609
3610fn current_execution_hash(
3611    intent_id: i64,
3612    hashes: &[ExecutionTransactionHashRow],
3613) -> anyhow::Result<&ExecutionTransactionHashRow> {
3614    let mut current = hashes.iter().filter(|row| row.current);
3615    let row = current
3616        .next()
3617        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} has no current hash"))?;
3618    anyhow::ensure!(
3619        current.next().is_none(),
3620        "Execution intent {intent_id} has more than one current hash"
3621    );
3622    Ok(row)
3623}
3624
3625fn open_execution_payload(
3626    keys: &PayloadKeySet,
3627    policy: PayloadPolicy,
3628    intent: &ExecutionIntentRow,
3629    hash: &ExecutionTransactionHashRow,
3630    reason: &str,
3631) -> anyhow::Result<Vec<u8>> {
3632    anyhow::ensure!(
3633        hash.payload_expected,
3634        "Execution transaction {} has no signed payload",
3635        hash.transaction_hash
3636    );
3637    anyhow::ensure!(
3638        hash.raw_transaction.is_none(),
3639        "Protected execution transaction {} contains plaintext",
3640        hash.transaction_hash
3641    );
3642    let envelope = hash.sealed_transaction.as_deref().ok_or_else(|| {
3643        anyhow::anyhow!(
3644            "Protected execution transaction {} has no sealed payload",
3645            hash.transaction_hash
3646        )
3647    })?;
3648    let context = payload_context(intent, hash, keys.deployment_id())?;
3649    let raw_transaction = keys.unseal(envelope, &context)?;
3650    log::info!(
3651        "Unsealed execution payload for intent {} transaction {} during {reason}",
3652        intent.id,
3653        hash.transaction_hash
3654    );
3655    authenticate_retained_payload(&raw_transaction, intent, hash, keys.deployment_id())?;
3656    if retained_payload_requires_policy(intent, hash, policy)? {
3657        authenticate_payload(&raw_transaction, intent, hash, policy, keys.deployment_id())
3658            .with_context(|| {
3659                format!(
3660                    "execution intent {} transaction {} violates current execution policy",
3661                    intent.id, hash.transaction_hash
3662                )
3663            })?;
3664    }
3665    Ok(raw_transaction)
3666}
3667
3668/// Derives the receipt poll budget from the configured inclusion timeout in seconds.
3669fn receipt_max_polls(receipt_timeout_secs: u64) -> u32 {
3670    u32::try_from(receipt_timeout_secs.max(1)).unwrap_or(u32::MAX)
3671}
3672
3673fn receipt_timeout(receipt_timeout_secs: u64) -> Duration {
3674    Duration::from_secs(receipt_timeout_secs.clamp(1, u64::from(u32::MAX)))
3675}
3676
3677/// A fully validated swap ready for asynchronous dispatch.
3678#[derive(Debug)]
3679struct SwapPlan {
3680    order: OrderAny,
3681    pool: Pool,
3682    quote_currency: Currency,
3683    instrument_id: InstrumentId,
3684    pool_address: Address,
3685    router: Address,
3686    factory: Address,
3687    weth: Address,
3688    token_in: Address,
3689    token_out: Address,
3690    fee: U24,
3691    amount_in: U256,
3692    min_amount_out: U256,
3693    slippage_bps: u32,
3694    quote_spend_ceiling: Option<QuoteSpendCeiling>,
3695    profiler_position: Option<BlockPosition>,
3696}
3697
3698#[derive(Debug, Clone)]
3699struct SwapQuoteAnchors {
3700    watermark: VerifiedBlockHeader,
3701    state: VerifiedBlockHeader,
3702    quote_contract: Address,
3703    token_in: Address,
3704    token_out: Address,
3705    fee: U24,
3706    quote_kind: SwapQuoteKind,
3707    quote: UniswapV3Quote,
3708    precondition_decisions: Vec<ExecutionVerificationDecision>,
3709}
3710
3711#[derive(Debug, Clone, Copy)]
3712enum SwapQuoteKind {
3713    ExactInput(U256),
3714    ExactOutput(U256),
3715}
3716
3717/// Executes a validated swap plan: read-only pre-trade checks, signing, persistence,
3718/// broadcast, and finality observation.
3719///
3720/// Emits only events justified by known transaction state: `OrderDenied` before the
3721/// transaction exists, `OrderSubmitted` after broadcast acceptance, and `OrderRejected` on
3722/// a definitive node rejection or an on-chain revert. No fill is emitted at broadcast or
3723/// first inclusion; fills arrive with finality reconciliation. Ambiguous outcomes keep the
3724/// order submitted untouched while the persisted record reconciles.
3725async fn execute_swap(
3726    mut plan: SwapPlan,
3727    executor: TransactionExecutor,
3728    emitter: ExecutionEventEmitter,
3729    max_quote_age_blocks: u64,
3730    deadline_seconds: u64,
3731) -> anyhow::Result<()> {
3732    let order = &plan.order;
3733
3734    if let Err(e) = executor.claim_slot(TransactionPurpose::Swap) {
3735        emitter.emit_order_denied(order, &e.to_string());
3736        return Ok(());
3737    }
3738
3739    let Some(profiler_position) = plan.profiler_position.as_ref() else {
3740        release_preparing_slot(&executor.in_flight);
3741        emitter.emit_order_denied(order, "Pool profiler has no quote provenance");
3742        return Ok(());
3743    };
3744    let mut swap_anchors = match validate_swap_quote(
3745        profiler_position,
3746        &plan,
3747        max_quote_age_blocks,
3748        &executor.verification,
3749        &executor.deployment_manifest,
3750    )
3751    .await
3752    {
3753        Ok(anchors) => anchors,
3754        Err(e) => {
3755            release_preparing_slot(&executor.in_flight);
3756            emitter.emit_order_denied(order, &e.to_string());
3757            return Ok(());
3758        }
3759    };
3760    let (amount_in, min_amount_out) = match verified_swap_amounts(&plan, swap_anchors.quote) {
3761        Ok(amounts) => amounts,
3762        Err(e) => {
3763            release_preparing_slot(&executor.in_flight);
3764            emitter.emit_order_denied(order, &e.to_string());
3765            return Ok(());
3766        }
3767    };
3768    plan.amount_in = amount_in;
3769    plan.min_amount_out = min_amount_out;
3770    let deadline = match swap_anchors.state.timestamp.checked_add(deadline_seconds) {
3771        Some(deadline) => deadline,
3772        None => {
3773            release_preparing_slot(&executor.in_flight);
3774            emitter.emit_order_denied(
3775                order,
3776                &format!(
3777                    "Swap deadline overflow: anchor timestamp {} plus `deadline_seconds` {deadline_seconds} exceeds u64",
3778                    swap_anchors.state.timestamp
3779                ),
3780            );
3781            return Ok(());
3782        }
3783    };
3784
3785    swap_anchors.precondition_decisions =
3786        match check_swap_preconditions(&plan, swap_anchors.state.number, &executor).await {
3787            Ok(decisions) => decisions,
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
3795    let calldata = UniswapV3SwapRouter::exactInputSingleCall {
3796        params: UniswapV3SwapRouter::ExactInputSingleParams {
3797            tokenIn: plan.token_in,
3798            tokenOut: plan.token_out,
3799            fee: plan.fee,
3800            recipient: executor.wallet_address,
3801            deadline: U256::from(deadline),
3802            amountIn: plan.amount_in,
3803            amountOutMinimum: plan.min_amount_out,
3804            sqrtPriceLimitX96: U160::ZERO,
3805        },
3806    }
3807    .abi_encode();
3808
3809    let calldata = Bytes::from(calldata);
3810    let intent = ExecutionIntentInsert {
3811        chain_id: executor.chain_id,
3812        wallet_address: executor.wallet_address.to_string(),
3813        purpose: TransactionPurpose::Swap.as_str().to_string(),
3814        client_order_id: Some(order.client_order_id().to_string()),
3815        trader_id: Some(order.trader_id().to_string()),
3816        strategy_id: Some(order.strategy_id().to_string()),
3817        account_id: Some(executor.account_id.to_string()),
3818        instrument_id: Some(plan.instrument_id.to_string()),
3819        pool_address: Some(plan.pool_address.to_string()),
3820        transaction_to: plan.router.to_string(),
3821        transaction_input: hex::encode_prefixed(&calldata),
3822        transaction_value: U256::ZERO.to_string(),
3823        amount_in: Some(plan.amount_in.to_string()),
3824        created_block: swap_anchors.state.number,
3825    };
3826    let intent = match executor.database.reserve_execution_intent(&intent).await {
3827        Ok(intent) => intent,
3828        Err(e) => {
3829            release_preparing_if_reservation_not_committed(&executor.in_flight, &e);
3830            emitter.emit_order_denied(order, &e.to_string());
3831            return Ok(());
3832        }
3833    };
3834    let prepared = match executor
3835        .prepare_and_sign_swap(
3836            intent.id,
3837            intent.created_block,
3838            plan.router,
3839            U256::ZERO,
3840            calldata,
3841            &swap_anchors,
3842        )
3843        .await
3844    {
3845        Ok(prepared) => prepared,
3846        Err(e) => {
3847            if executor
3848                .database
3849                .mark_execution_intent_recoverable(intent.id)
3850                .await
3851                .is_ok()
3852            {
3853                release_preparing_slot(&executor.in_flight);
3854            }
3855            emitter.emit_order_denied(order, &e.to_string());
3856            return Ok(());
3857        }
3858    };
3859
3860    if let Err(e) = executor
3861        .fill_and_persist(&prepared, TransactionPurpose::Swap)
3862        .await
3863    {
3864        emitter.emit_order_denied(order, &e.to_string());
3865        return Ok(());
3866    }
3867
3868    let broadcast = executor.broadcast(&prepared).await?;
3869    emitter.emit_order_submitted(order);
3870    executor
3871        .database
3872        .mark_execution_event_emitted(intent.id, "acknowledgement")
3873        .await?;
3874
3875    if let BroadcastOutcome::Ambiguous(message) = broadcast {
3876        log::warn!("{message}");
3877    }
3878
3879    let trace_purpose = match plan.order.order_side() {
3880        OrderSide::Sell => "swap_sell",
3881        OrderSide::Buy => "swap_buy",
3882    };
3883
3884    match executor.await_finality(&prepared).await? {
3885        InclusionOutcome::Finalized(mut included) => {
3886            included.finality.decisions.extend(
3887                verify_finalized_transaction(
3888                    &included,
3889                    &intent,
3890                    prepared.nonce,
3891                    &prepared.raw_tx,
3892                    &executor,
3893                    trace_purpose,
3894                )
3895                .await?,
3896            );
3897            let fill = validate_finalized_swap_fill(&plan, &included)?;
3898            let wallet = load_verified_wallet_after_fill(&plan, &included, &executor).await?;
3899            executor
3900                .commit_verified_finality(
3901                    &included,
3902                    TransactionStatus::Finalized,
3903                    &wallet.decisions,
3904                )
3905                .await?;
3906            complete_finalized_swap(
3907                &plan,
3908                intent.id,
3909                included.tx_hash,
3910                fill,
3911                wallet,
3912                &executor,
3913                &emitter,
3914            )
3915            .await?;
3916            executor.release_slot();
3917            Ok(())
3918        }
3919        InclusionOutcome::Reverted(mut included) => {
3920            included.finality.decisions.extend(
3921                verify_finalized_transaction(
3922                    &included,
3923                    &intent,
3924                    prepared.nonce,
3925                    &prepared.raw_tx,
3926                    &executor,
3927                    trace_purpose,
3928                )
3929                .await?,
3930            );
3931            executor
3932                .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
3933                .await?;
3934            send_reverted_order(&emitter, order, &included)?;
3935            executor
3936                .database
3937                .mark_execution_event_emitted(intent.id, "terminal")
3938                .await?;
3939            executor.release_slot();
3940            Ok(())
3941        }
3942        InclusionOutcome::Pending(message) => anyhow::bail!(message),
3943    }
3944}
3945
3946async fn validate_swap_quote(
3947    position: &BlockPosition,
3948    plan: &SwapPlan,
3949    max_age_blocks: u64,
3950    verification: &VerificationCoordinator,
3951    manifest: &BlockchainDeploymentManifest,
3952) -> anyhow::Result<SwapQuoteAnchors> {
3953    let block_hash = position.block_hash.as_deref().ok_or_else(|| {
3954        anyhow::anyhow!(
3955            "Pool state at block {} has no ingestion-time block hash; refresh the profiler before execution",
3956            position.number
3957        )
3958    })?;
3959    let expected_block_hash = B256::from_str(block_hash)
3960        .with_context(|| format!("Invalid profiler block hash {block_hash}"))?;
3961    let now_unix_secs = current_unix_secs()?;
3962    let head = verified_value(
3963        verification.verify_decision_header(now_unix_secs).await,
3964        "swap decision header",
3965    )?;
3966    validate_quote_age(position.number, head.number, max_age_blocks)?;
3967    let canonical_block = verified_value(
3968        verification.verify_block(position.number).await,
3969        "profiler watermark header",
3970    )?;
3971    anyhow::ensure!(
3972        canonical_block.hash == expected_block_hash,
3973        "Pool state block {} changed from {} to {}; refresh the profiler before execution",
3974        position.number,
3975        expected_block_hash,
3976        canonical_block.hash
3977    );
3978
3979    let snapshot_transaction = position.transaction_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
3980    let snapshot_log = position.log_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
3981    anyhow::ensure!(
3982        snapshot_transaction == snapshot_log,
3983        "Pool state at block {} has an invalid partial snapshot watermark",
3984        position.number
3985    );
3986
3987    if snapshot_transaction {
3988        let snapshot_hash = B256::from_str(&position.transaction_hash)
3989            .with_context(|| "Invalid block-scoped snapshot hash")?;
3990        anyhow::ensure!(
3991            snapshot_hash == expected_block_hash,
3992            "Block-scoped snapshot hash {snapshot_hash} does not match ingestion hash {expected_block_hash}"
3993        );
3994    } else {
3995        validate_profiler_event_verified(
3996            position,
3997            expected_block_hash,
3998            plan.pool_address,
3999            &plan.pool,
4000            verification,
4001        )
4002        .await?;
4003    }
4004
4005    let ancestry = verified_value(
4006        verification
4007            .verify_header_window(canonical_block, head.number)
4008            .await,
4009        "profiler-to-decision ancestry",
4010    )?;
4011
4012    if let Some(last) = ancestry.last() {
4013        anyhow::ensure!(
4014            *last == head,
4015            "Swap decision header conflicts with profiler ancestry"
4016        );
4017    } else {
4018        anyhow::ensure!(
4019            canonical_block == head,
4020            "Empty profiler ancestry does not end at the decision header"
4021        );
4022    }
4023    verified_value(
4024        verification
4025            .verify_deployment_manifest(manifest, head.number)
4026            .await,
4027        "swap deployment manifest",
4028    )?;
4029    let quote_contract = validate_manifest_pool(plan, manifest)?;
4030    let quote_kind = match plan.order.order_side() {
4031        OrderSide::Sell => SwapQuoteKind::ExactInput(quantity_to_raw_amount(
4032            plan.order.quantity(),
4033            plan.pool.get_base_token().decimals,
4034        )?),
4035        OrderSide::Buy => SwapQuoteKind::ExactOutput(quantity_to_raw_amount(
4036            plan.order.quantity(),
4037            plan.pool.get_base_token().decimals,
4038        )?),
4039    };
4040    let quote = verified_value(
4041        verify_swap_quote(verification, quote_contract, plan, quote_kind, head.number).await,
4042        "independent swap quote",
4043    )?;
4044
4045    Ok(SwapQuoteAnchors {
4046        watermark: canonical_block,
4047        state: head,
4048        quote_contract,
4049        token_in: plan.token_in,
4050        token_out: plan.token_out,
4051        fee: plan.fee,
4052        quote_kind,
4053        quote,
4054        precondition_decisions: Vec::new(),
4055    })
4056}
4057
4058async fn verify_swap_quote(
4059    verification: &VerificationCoordinator,
4060    quote_contract: Address,
4061    plan: &SwapPlan,
4062    kind: SwapQuoteKind,
4063    block: u64,
4064) -> VerificationOutcome<UniswapV3Quote> {
4065    match kind {
4066        SwapQuoteKind::ExactInput(amount_in) => {
4067            verification
4068                .verify_quote_exact_input_single(
4069                    &quote_contract,
4070                    plan.token_in,
4071                    plan.token_out,
4072                    amount_in,
4073                    plan.fee,
4074                    block,
4075                )
4076                .await
4077        }
4078        SwapQuoteKind::ExactOutput(amount_out) => {
4079            verification
4080                .verify_quote_exact_output_single(
4081                    &quote_contract,
4082                    plan.token_in,
4083                    plan.token_out,
4084                    amount_out,
4085                    plan.fee,
4086                    block,
4087                )
4088                .await
4089        }
4090    }
4091}
4092
4093fn verified_swap_amounts(plan: &SwapPlan, quote: UniswapV3Quote) -> anyhow::Result<(U256, U256)> {
4094    anyhow::ensure!(
4095        !quote.amount.is_zero(),
4096        "Independent swap quote returned zero"
4097    );
4098    let base_amount =
4099        quantity_to_raw_amount(plan.order.quantity(), plan.pool.get_base_token().decimals)?;
4100    let slippage_bps = plan.slippage_bps;
4101    match plan.order.order_side() {
4102        OrderSide::Sell => Ok((
4103            base_amount,
4104            derive_min_amount_out(quote.amount, slippage_bps)?,
4105        )),
4106        OrderSide::Buy => {
4107            let ceiling = plan.quote_spend_ceiling.ok_or_else(|| {
4108                anyhow::anyhow!(
4109                    "No quote spend ceiling for BUY token pair {} -> {}",
4110                    plan.token_in,
4111                    plan.token_out
4112                )
4113            })?;
4114            anyhow::ensure!(
4115                quote.amount <= ceiling.max_amount,
4116                "BUY quote amount {} exceeds the configured quote-spend maximum {} for {} -> {}",
4117                quote.amount,
4118                ceiling.max_amount,
4119                plan.token_in,
4120                plan.token_out
4121            );
4122            Ok((
4123                quote.amount,
4124                derive_min_amount_out(base_amount, slippage_bps)?,
4125            ))
4126        }
4127    }
4128}
4129
4130fn validate_manifest_pool(
4131    plan: &SwapPlan,
4132    manifest: &BlockchainDeploymentManifest,
4133) -> anyhow::Result<Address> {
4134    let matching = manifest
4135        .pools
4136        .iter()
4137        .filter(|pool| Address::from_str(&pool.address).ok() == Some(plan.pool_address))
4138        .collect::<Vec<_>>();
4139    anyhow::ensure!(
4140        matching.len() == 1,
4141        "Pool {} does not have exactly one deployment manifest definition",
4142        plan.pool_address
4143    );
4144    let pool = matching[0];
4145    let token0 = Address::from_str(&pool.token0)?;
4146    let token1 = Address::from_str(&pool.token1)?;
4147    let factory = Address::from_str(&pool.factory)?;
4148    let quote_contract = Address::from_str(&pool.quote_contract)?;
4149    anyhow::ensure!(
4150        token0 == plan.pool.token0.address
4151            && token1 == plan.pool.token1.address
4152            && pool.fee == plan.pool.fee.expect("validated pool fee")
4153            && factory == plan.factory,
4154        "Cached pool {} does not match its deployment manifest identity",
4155        plan.pool_address
4156    );
4157
4158    for token in [&plan.pool.token0, &plan.pool.token1] {
4159        let identities = manifest
4160            .tokens
4161            .iter()
4162            .filter(|identity| Address::from_str(&identity.address).ok() == Some(token.address))
4163            .collect::<Vec<_>>();
4164        anyhow::ensure!(
4165            identities.len() == 1,
4166            "Token {} does not have exactly one deployment manifest identity",
4167            token.address
4168        );
4169        let identity = identities[0];
4170        anyhow::ensure!(
4171            identity.name == token.name
4172                && identity.symbol == token.symbol
4173                && identity.decimals == token.decimals,
4174            "Cached token {} does not match its deployment manifest identity",
4175            token.address
4176        );
4177        let expected_role = if token.address == plan.pool.get_base_token().address {
4178            "base"
4179        } else {
4180            "quote"
4181        };
4182        anyhow::ensure!(
4183            matches!(identity.asset_role.as_str(), "both") || identity.asset_role == expected_role,
4184            "Token {} is not permitted as the pool {expected_role} asset",
4185            token.address
4186        );
4187    }
4188    Ok(quote_contract)
4189}
4190
4191async fn validate_profiler_event_verified(
4192    position: &BlockPosition,
4193    expected_block_hash: B256,
4194    pool_address: Address,
4195    pool: &Pool,
4196    verification: &VerificationCoordinator,
4197) -> anyhow::Result<()> {
4198    let transaction_hash = B256::from_str(&position.transaction_hash).with_context(|| {
4199        format!(
4200            "Invalid profiler transaction hash {}",
4201            position.transaction_hash
4202        )
4203    })?;
4204    let receipt = verified_value(
4205        verification.verify_receipt(&transaction_hash).await,
4206        "profiler watermark receipt",
4207    )?;
4208    anyhow::ensure!(
4209        receipt.status,
4210        "Profiler transaction did not execute successfully"
4211    );
4212    anyhow::ensure!(
4213        receipt.transaction_hash == transaction_hash,
4214        "Profiler receipt transaction hash does not match its ingestion watermark"
4215    );
4216    anyhow::ensure!(
4217        receipt.block_number == position.number
4218            && receipt.block_hash == expected_block_hash
4219            && receipt.transaction_index == u64::from(position.transaction_index),
4220        "Profiler receipt position does not match its ingestion watermark"
4221    );
4222    let matching_logs = receipt
4223        .logs
4224        .iter()
4225        .filter(|log| rpc_helpers::extract_log_index(log).ok() == Some(position.log_index))
4226        .collect::<Vec<_>>();
4227    anyhow::ensure!(
4228        matching_logs.len() == 1,
4229        "Profiler receipt contains {} logs at global index {}; expected exactly one",
4230        matching_logs.len(),
4231        position.log_index
4232    );
4233    let log = matching_logs[0];
4234    let log_transaction_hash = B256::from_str(&rpc_helpers::extract_transaction_hash(log)?)
4235        .with_context(|| "Invalid profiler log transaction hash")?;
4236    let log_block_hash = log
4237        .block_hash
4238        .as_deref()
4239        .ok_or_else(|| anyhow::anyhow!("Profiler log has no block hash"))?;
4240    anyhow::ensure!(
4241        !log.removed
4242            && log_transaction_hash == transaction_hash
4243            && rpc_helpers::extract_block_number(log)? == position.number
4244            && rpc_helpers::extract_transaction_index(log)? == position.transaction_index
4245            && B256::from_str(log_block_hash)? == expected_block_hash,
4246        "Profiler log position does not match its ingestion watermark"
4247    );
4248    anyhow::ensure!(
4249        rpc_helpers::extract_address(log)? == pool_address,
4250        "Profiler watermark log did not come from expected pool {pool_address}"
4251    );
4252    let signature = log
4253        .topics
4254        .first()
4255        .ok_or_else(|| anyhow::anyhow!("Profiler watermark log has no event signature"))?;
4256    let supported =
4257        profiler_event_signatures(pool).any(|expected| expected.eq_ignore_ascii_case(signature));
4258    anyhow::ensure!(
4259        supported,
4260        "Profiler watermark log has an unsupported event signature"
4261    );
4262    Ok(())
4263}
4264
4265fn profiler_event_signatures(pool: &Pool) -> impl Iterator<Item = &str> {
4266    [
4267        Some(pool.dex.swap_created_event.as_ref()),
4268        Some(pool.dex.mint_created_event.as_ref()),
4269        Some(pool.dex.burn_created_event.as_ref()),
4270        Some(pool.dex.collect_created_event.as_ref()),
4271        pool.dex.flash_created_event.as_deref(),
4272        pool.dex.fee_protocol_update_event.as_deref(),
4273        pool.dex.fee_protocol_collect_event.as_deref(),
4274    ]
4275    .into_iter()
4276    .flatten()
4277}
4278
4279fn validate_quote_age(
4280    profiler_block: u64,
4281    latest_block: u64,
4282    max_age_blocks: u64,
4283) -> anyhow::Result<()> {
4284    anyhow::ensure!(
4285        profiler_block <= latest_block,
4286        "Pool state at block {profiler_block} is ahead of the latest block {latest_block}; the execution RPC endpoint lags the data feed"
4287    );
4288    let quote_age = latest_block - profiler_block;
4289    anyhow::ensure!(
4290        quote_age <= max_age_blocks,
4291        "Stale quote: pool state at block {profiler_block}, latest block {latest_block}, exceeds `max_quote_age_blocks` {max_age_blocks}"
4292    );
4293    Ok(())
4294}
4295
4296fn validate_rpc_transaction_matches_payload(
4297    transaction: &RpcTransaction,
4298    raw_transaction: &[u8],
4299) -> anyhow::Result<()> {
4300    let signed = decode_signed_transaction(raw_transaction)?;
4301    anyhow::ensure!(
4302        transaction.hash == signed.hash
4303            && transaction.from == signed.signer
4304            && transaction.nonce == signed.nonce
4305            && transaction.chain_id == Some(signed.chain_id)
4306            && transaction.transaction_type == Some(2)
4307            && transaction.to == Some(signed.to)
4308            && transaction.input == signed.input
4309            && transaction.value == signed.value
4310            && transaction.gas == Some(signed.gas_limit)
4311            && transaction.max_fee_per_gas == Some(U256::from(signed.max_fee_per_gas))
4312            && transaction.max_priority_fee_per_gas
4313                == Some(U256::from(signed.max_priority_fee_per_gas)),
4314        "Verified transaction fields differ from the authenticated signed payload"
4315    );
4316    Ok(())
4317}
4318
4319async fn verify_finalized_transaction(
4320    included: &IncludedTransaction,
4321    intent: &ExecutionIntentRow,
4322    nonce: u64,
4323    raw_transaction: &[u8],
4324    executor: &TransactionExecutor,
4325    trace_purpose: &str,
4326) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4327    verify_finalized_transaction_identity(
4328        included,
4329        intent,
4330        nonce,
4331        raw_transaction,
4332        &executor.verification,
4333        executor.wallet_address,
4334        executor.chain_id,
4335        &executor.deployment_manifest,
4336        trace_purpose,
4337    )
4338    .await
4339}
4340
4341#[expect(clippy::too_many_arguments)]
4342async fn verify_finalized_transaction_identity(
4343    included: &IncludedTransaction,
4344    intent: &ExecutionIntentRow,
4345    nonce: u64,
4346    raw_transaction: &[u8],
4347    verification: &VerificationCoordinator,
4348    wallet_address: Address,
4349    chain_id: u32,
4350    deployment_manifest: &BlockchainDeploymentManifest,
4351    trace_purpose: &str,
4352) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4353    let transaction_verification = required_verification(
4354        verification.verify_transaction(&included.tx_hash).await,
4355        "finalized transaction",
4356    )?;
4357    let transaction = &transaction_verification.value;
4358    let signed = decode_signed_transaction(raw_transaction)?;
4359    let (expected_to, expected_input, expected_value) = persisted_call_fields(intent)?;
4360    anyhow::ensure!(
4361        included.receipt.transaction_hash == included.tx_hash
4362            && signed.hash == included.tx_hash
4363            && signed.signer == wallet_address
4364            && signed.chain_id == u64::from(chain_id)
4365            && signed.nonce == nonce
4366            && signed.to == expected_to
4367            && signed.input == expected_input
4368            && signed.value == expected_value,
4369        "Finalized transaction does not match the authenticated signed payload and persisted intent"
4370    );
4371    validate_rpc_transaction_matches_payload(transaction, raw_transaction)
4372        .context("finalized transaction identity mismatch")?;
4373
4374    let trace_verification = required_verification(
4375        verification.verify_call_trace(&included.tx_hash).await,
4376        "finalized call trace",
4377    )?;
4378    validate_call_trace(
4379        &trace_verification.value,
4380        &signed,
4381        included.receipt.status,
4382        trace_purpose,
4383        deployment_manifest,
4384    )?;
4385    let deployment_verification = required_verification(
4386        verification
4387            .verify_deployment_manifest(deployment_manifest, included.block_number)
4388            .await,
4389        "inclusion deployment manifest",
4390    )?;
4391
4392    Ok(vec![
4393        verification_decision(
4394            &transaction_verification,
4395            Some(included.block_number),
4396            Some(included.block_number),
4397        ),
4398        verification_decision(
4399            &trace_verification,
4400            Some(included.block_number),
4401            Some(included.block_number),
4402        ),
4403        verification_decision(
4404            &deployment_verification,
4405            Some(included.block_number),
4406            Some(included.block_number),
4407        ),
4408    ])
4409}
4410
4411fn validate_call_trace(
4412    trace: &VerifiedCallTrace,
4413    signed: &crate::execution::transaction::DecodedSignedTransaction,
4414    receipt_success: bool,
4415    purpose: &str,
4416    manifest: &BlockchainDeploymentManifest,
4417) -> anyhow::Result<()> {
4418    anyhow::ensure!(
4419        trace.call_type == RpcCallType::Call
4420            && trace.from == signed.signer
4421            && trace.to == Some(signed.to)
4422            && trace.value == signed.value
4423            && trace.input_digest == keccak256(&signed.input)
4424            && trace.success == receipt_success,
4425        "Verified call-trace root differs from the authenticated transaction"
4426    );
4427    validate_internal_calls(&trace.calls, signed.to, purpose, manifest)
4428}
4429
4430fn validate_internal_calls(
4431    calls: &[VerifiedCallTrace],
4432    caller_context: Address,
4433    purpose: &str,
4434    manifest: &BlockchainDeploymentManifest,
4435) -> anyhow::Result<()> {
4436    for call in calls {
4437        anyhow::ensure!(
4438            call.from == caller_context,
4439            "Verified call trace child has an invalid caller context"
4440        );
4441        let target = call.to.ok_or_else(|| {
4442            anyhow::anyhow!("Verified call trace contains an operation without a target")
4443        })?;
4444        let call_type = match call.call_type {
4445            RpcCallType::Call => "call",
4446            RpcCallType::Callcode => "callcode",
4447            RpcCallType::Delegatecall => "delegatecall",
4448            RpcCallType::Staticcall => "staticcall",
4449            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4450                anyhow::bail!("Verified call trace contains a forbidden state-changing operation")
4451            }
4452        };
4453        let permitted = manifest.call_edges.iter().any(|edge| {
4454            edge.purpose == purpose
4455                && edge.call_type.eq_ignore_ascii_case(call_type)
4456                && Address::from_str(&edge.caller).ok() == Some(call.from)
4457                && Address::from_str(&edge.target).ok() == Some(target)
4458        });
4459        anyhow::ensure!(
4460            permitted,
4461            "Verified call trace contains an unreviewed {call_type} edge {} -> {target} for {purpose}",
4462            call.from
4463        );
4464        let child_context = match call.call_type {
4465            RpcCallType::Call | RpcCallType::Staticcall => target,
4466            RpcCallType::Callcode | RpcCallType::Delegatecall => caller_context,
4467            RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4468                unreachable!("forbidden operations return before child traversal")
4469            }
4470        };
4471        validate_internal_calls(&call.calls, child_context, purpose, manifest)?;
4472    }
4473    Ok(())
4474}
4475
4476async fn verify_wrap_balance_increase(
4477    executor: &TransactionExecutor,
4478    weth_address: &Address,
4479    amount_wei: U256,
4480    included: &IncludedTransaction,
4481) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4482    let previous_block = included.block_number.checked_sub(1).ok_or_else(|| {
4483        anyhow::anyhow!(
4484            "Included wrap transaction {} has invalid block number 0",
4485            included.tx_hash
4486        )
4487    })?;
4488    let call = ERC20::balanceOfCall {
4489        account: executor.wallet_address,
4490    }
4491    .abi_encode();
4492    let balance_before = required_verification(
4493        executor
4494            .verification
4495            .verify_decoded_call(
4496                None,
4497                weth_address,
4498                U256::ZERO,
4499                &call,
4500                previous_block,
4501                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4502            )
4503            .await,
4504        "wrapped balance before finality",
4505    )
4506    .with_context(|| {
4507        format!(
4508            "failed to verify WETH balance before included transaction {} at block {previous_block}",
4509            included.tx_hash
4510        )
4511    })?;
4512    let balance_after = required_verification(
4513        executor
4514            .verification
4515            .verify_decoded_call(
4516                None,
4517                weth_address,
4518                U256::ZERO,
4519                &call,
4520                included.block_number,
4521                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4522            )
4523            .await,
4524        "wrapped balance after finality",
4525    )
4526    .with_context(|| {
4527        format!(
4528            "failed to verify WETH balance after included transaction {} at block {}",
4529            included.tx_hash, included.block_number
4530        )
4531    })?;
4532    let expected_balance = balance_before
4533        .value
4534        .checked_add(amount_wei)
4535        .ok_or_else(|| {
4536            anyhow::anyhow!(
4537                "WETH balance overflow for included transaction {} at block {}",
4538                included.tx_hash,
4539                included.block_number
4540            )
4541        })?;
4542    anyhow::ensure!(
4543        balance_after.value == expected_balance,
4544        "WETH balance after transaction {} did not increase by {amount_wei}: expected {expected_balance}, was {}",
4545        included.tx_hash,
4546        balance_after.value
4547    );
4548
4549    Ok(vec![
4550        verification_decision(&balance_before, Some(previous_block), Some(previous_block)),
4551        verification_decision(
4552            &balance_after,
4553            Some(included.block_number),
4554            Some(included.block_number),
4555        ),
4556    ])
4557}
4558
4559async fn verify_approve_allowance(
4560    executor: &TransactionExecutor,
4561    token: &Address,
4562    router: &Address,
4563    amount: U256,
4564    included: &IncludedTransaction,
4565) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4566    let call = ERC20::allowanceCall {
4567        owner: executor.wallet_address,
4568        spender: *router,
4569    }
4570    .abi_encode();
4571    let allowance = required_verification(
4572        executor
4573            .verification
4574            .verify_decoded_call(
4575                None,
4576                token,
4577                U256::ZERO,
4578                &call,
4579                included.block_number,
4580                |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
4581            )
4582            .await,
4583        "router allowance after finality",
4584    )
4585    .with_context(|| {
4586        format!(
4587            "failed to verify router allowance after included transaction {} at block {}",
4588            included.tx_hash, included.block_number
4589        )
4590    })?;
4591    anyhow::ensure!(
4592        allowance.value == amount,
4593        "Router allowance after transaction {} does not equal the requested amount {amount}: was {}",
4594        included.tx_hash,
4595        allowance.value
4596    );
4597
4598    Ok(vec![verification_decision(
4599        &allowance,
4600        Some(included.block_number),
4601        Some(included.block_number),
4602    )])
4603}
4604
4605async fn complete_finalized_swap(
4606    plan: &SwapPlan,
4607    intent_id: i64,
4608    tx_hash: B256,
4609    fill: Option<FinalizedSwapFill>,
4610    wallet: VerifiedWalletRefresh,
4611    executor: &TransactionExecutor,
4612    emitter: &ExecutionEventEmitter,
4613) -> anyhow::Result<()> {
4614    if let Some(fill) = fill {
4615        let filled = OrderFilled::new(
4616            emitter.trader_id(),
4617            plan.order.strategy_id(),
4618            plan.order.instrument_id(),
4619            plan.order.client_order_id(),
4620            fill.venue_order_id,
4621            emitter.account_id(),
4622            fill.trade_id,
4623            plan.order.order_side(),
4624            plan.order.order_type(),
4625            fill.last_qty,
4626            fill.last_px,
4627            plan.quote_currency,
4628            LiquiditySide::Taker,
4629            execution_event_id(tx_hash, b"fill"),
4630            fill.ts_event,
4631            fill.ts_event,
4632            false,
4633            None,
4634            Some(fill.commission),
4635            None,
4636        );
4637        emitter.try_send_order_event(OrderEventAny::Filled(filled))?;
4638
4639        if fill.last_qty < plan.order.quantity() {
4640            let canceled = OrderCanceled::new(
4641                emitter.trader_id(),
4642                plan.order.strategy_id(),
4643                plan.order.instrument_id(),
4644                plan.order.client_order_id(),
4645                execution_event_id(tx_hash, b"partial_cancel"),
4646                fill.ts_event,
4647                fill.ts_event,
4648                false,
4649                Some(fill.venue_order_id),
4650                Some(emitter.account_id()),
4651            );
4652            emitter.try_send_order_event(OrderEventAny::Canceled(canceled))?;
4653        }
4654    }
4655
4656    *executor.wallet_balance.lock() = wallet.wallet_balance;
4657    emitter.try_emit_account_state(
4658        wallet.balances,
4659        vec![],
4660        true,
4661        get_atomic_clock_realtime().get_time_ns(),
4662        None,
4663    )?;
4664    executor
4665        .database
4666        .mark_execution_event_emitted(intent_id, "fill")
4667        .await
4668}
4669
4670fn send_reverted_order(
4671    emitter: &ExecutionEventEmitter,
4672    order: &OrderAny,
4673    included: &IncludedTransaction,
4674) -> anyhow::Result<()> {
4675    let ts_event = finalized_inclusion_time(included)?;
4676    let rejected = OrderRejected::new(
4677        emitter.trader_id(),
4678        order.strategy_id(),
4679        order.instrument_id(),
4680        order.client_order_id(),
4681        emitter.account_id(),
4682        format!("Transaction {} reverted on-chain", included.tx_hash).into(),
4683        execution_event_id(included.tx_hash, b"reverted"),
4684        ts_event,
4685        ts_event,
4686        false,
4687        false,
4688    );
4689    emitter.try_send_order_event(OrderEventAny::Rejected(rejected))
4690}
4691
4692fn finalized_inclusion_time(included: &IncludedTransaction) -> anyhow::Result<UnixNanos> {
4693    let inclusion = &included.finality.inclusion_header;
4694    anyhow::ensure!(
4695        inclusion.number == included.block_number
4696            && inclusion.hash == included.receipt.block_hash.to_string(),
4697        "Verified inclusion header does not match the finalized receipt"
4698    );
4699    let timestamp = inclusion.timestamp;
4700    let nanos = timestamp
4701        .checked_mul(NANOSECONDS_IN_SECOND)
4702        .ok_or_else(|| anyhow::anyhow!("Verified inclusion timestamp exceeds nanoseconds"))?;
4703    Ok(UnixNanos::from(nanos))
4704}
4705
4706fn execution_event_id(tx_hash: B256, event: &[u8]) -> UUID4 {
4707    let mut identity = Vec::with_capacity(tx_hash.len() + event.len());
4708    identity.extend_from_slice(tx_hash.as_slice());
4709    identity.extend_from_slice(event);
4710    let digest = keccak256(identity);
4711    let mut bytes = [0u8; 16];
4712    bytes.copy_from_slice(&digest[..16]);
4713    UUID4::from_bytes(bytes)
4714}
4715
4716struct FinalizedSwapFill {
4717    venue_order_id: VenueOrderId,
4718    trade_id: TradeId,
4719    last_qty: Quantity,
4720    last_px: Price,
4721    commission: Money,
4722    ts_event: UnixNanos,
4723}
4724
4725struct VerifiedWalletRefresh {
4726    wallet_balance: WalletBalance,
4727    balances: Vec<AccountBalance>,
4728    decisions: Vec<ExecutionVerificationDecision>,
4729}
4730
4731fn validate_finalized_swap_fill(
4732    plan: &SwapPlan,
4733    included: &IncludedTransaction,
4734) -> anyhow::Result<Option<FinalizedSwapFill>> {
4735    let signature =
4736        keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)").to_string();
4737    let swap_logs = included
4738        .receipt
4739        .logs
4740        .iter()
4741        .filter(|log| {
4742            !log.removed
4743                && log.topics.first().is_some_and(|topic| topic == &signature)
4744                && Address::from_str(&log.address).ok() == Some(plan.pool_address)
4745        })
4746        .collect::<Vec<_>>();
4747    anyhow::ensure!(
4748        swap_logs.len() == 1,
4749        "Finalized transaction {} emitted {} Swap logs from expected pool {}; expected exactly one",
4750        included.tx_hash,
4751        swap_logs.len(),
4752        plan.pool_address
4753    );
4754    let log = swap_logs[0];
4755    let log_transaction_hash = B256::from_str(&rpc_helpers::extract_transaction_hash(log)?)
4756        .with_context(|| "Invalid finalized Swap log transaction hash")?;
4757    let log_block_hash = log
4758        .block_hash
4759        .as_deref()
4760        .ok_or_else(|| anyhow::anyhow!("Finalized Swap log has no block hash"))?;
4761    anyhow::ensure!(
4762        log_transaction_hash == included.tx_hash
4763            && rpc_helpers::extract_block_number(log)? == included.block_number
4764            && u64::from(rpc_helpers::extract_transaction_index(log)?)
4765                == included.receipt.transaction_index
4766            && B256::from_str(log_block_hash)
4767                .with_context(|| "Invalid finalized Swap log block hash")?
4768                == included.receipt.block_hash,
4769        "Finalized Swap log position does not match transaction {}",
4770        included.tx_hash
4771    );
4772
4773    let dex = crate::exchanges::get_dex_extended(plan.pool.chain.name, &plan.pool.dex.name)
4774        .ok_or_else(|| {
4775            anyhow::anyhow!(
4776                "No RPC Swap decoder for {}:{}",
4777                plan.pool.chain.name,
4778                plan.pool.dex.name
4779            )
4780        })?;
4781    let event = dex.parse_swap_event_rpc(log)?;
4782    let (base_amount, quote_amount) =
4783        if plan.pool.get_base_token().address == plan.pool.token0.address {
4784            (event.amount0, event.amount1)
4785        } else {
4786            (event.amount1, event.amount0)
4787        };
4788    let last_qty = match plan.order.order_side() {
4789        OrderSide::Sell => {
4790            anyhow::ensure!(
4791                base_amount.is_positive() && base_amount.unsigned_abs() == plan.amount_in,
4792                "Finalized Swap input {base_amount} does not match the persisted amount {}",
4793                plan.amount_in
4794            );
4795            plan.order.quantity()
4796        }
4797        OrderSide::Buy => {
4798            anyhow::ensure!(
4799                quote_amount.is_positive() && quote_amount.unsigned_abs() == plan.amount_in,
4800                "Finalized Swap input {quote_amount} does not match the persisted amount {}",
4801                plan.amount_in
4802            );
4803            anyhow::ensure!(
4804                base_amount.is_negative(),
4805                "Finalized Swap base amount {base_amount} is not a BUY output"
4806            );
4807            raw_amount_to_quantity(
4808                base_amount.unsigned_abs(),
4809                plan.pool.get_base_token().decimals,
4810            )?
4811        }
4812    };
4813
4814    let block = &included.finality.inclusion_header;
4815    anyhow::ensure!(
4816        block.number == included.block_number
4817            && block.hash == included.receipt.block_hash.to_string(),
4818        "Verified inclusion header {} does not match receipt hash {}",
4819        included.block_number,
4820        included.receipt.block_hash
4821    );
4822    let timestamp_ns = block
4823        .timestamp
4824        .checked_mul(NANOSECONDS_IN_SECOND)
4825        .ok_or_else(|| anyhow::anyhow!("Finalized block timestamp overflows nanoseconds"))?;
4826    let mut swap = event.to_pool_swap(
4827        plan.pool.chain.clone(),
4828        plan.instrument_id,
4829        plan.pool.pool_identifier,
4830        UnixNanos::from(timestamp_ns),
4831    );
4832    swap.calculate_trade_info(&plan.pool.token0, &plan.pool.token1, None)?;
4833    let trade = swap
4834        .trade_info
4835        .as_ref()
4836        .ok_or_else(|| anyhow::anyhow!("Finalized Swap has no calculated trade information"))?;
4837    anyhow::ensure!(
4838        trade.order_side == plan.order.order_side(),
4839        "Finalized Swap side {} does not match {} order",
4840        trade.order_side,
4841        plan.order.order_side()
4842    );
4843    let gas_cost = included
4844        .receipt
4845        .effective_gas_price
4846        .checked_mul(U256::from(included.receipt.gas_used))
4847        .ok_or_else(|| anyhow::anyhow!("Finalized transaction gas commission overflow"))?;
4848    let commission = Money::from_u256(gas_cost, plan.pool.chain.native_currency())?;
4849    let trade_digest = keccak256(format!("{}:{}", included.tx_hash, swap.log_index));
4850    let trade_digest = trade_digest.to_string();
4851    let trade_id = TradeId::new_checked(&trade_digest[2..38])?;
4852
4853    if plan
4854        .order
4855        .trade_ids()
4856        .iter()
4857        .any(|existing| **existing == trade_id)
4858    {
4859        return Ok(None);
4860    }
4861
4862    let venue_order_id = VenueOrderId::new_checked(included.tx_hash.to_string())?;
4863    let ts_event = UnixNanos::from(timestamp_ns);
4864    let last_px = match plan.order.order_side() {
4865        OrderSide::Buy => fill_price_from_quote(last_qty, plan.amount_in, plan.quote_currency)?,
4866        _ => trade.execution_price,
4867    };
4868    Ok(Some(FinalizedSwapFill {
4869        venue_order_id,
4870        trade_id,
4871        last_qty,
4872        last_px,
4873        commission,
4874        ts_event,
4875    }))
4876}
4877
4878async fn load_verified_wallet_after_fill(
4879    plan: &SwapPlan,
4880    included: &IncludedTransaction,
4881    executor: &TransactionExecutor,
4882) -> anyhow::Result<VerifiedWalletRefresh> {
4883    let mut token_universe = executor.wallet_balance.lock().token_universe.clone();
4884    token_universe.insert(plan.pool.token0.address);
4885    token_universe.insert(plan.pool.token1.address);
4886
4887    let native_amount = required_verification(
4888        executor
4889            .verification
4890            .verify_balance(&executor.wallet_address, included.block_number)
4891            .await,
4892        "finalized native balance",
4893    )?;
4894    let native_balance = Money::from_u256(native_amount.value, plan.pool.chain.native_currency())?;
4895    let mut decisions = vec![verification_decision(
4896        &native_amount,
4897        Some(included.block_number),
4898        Some(included.block_number),
4899    )];
4900    let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
4901    token_addresses.sort_unstable();
4902    let mut token_balances = Vec::with_capacity(token_addresses.len());
4903    for address in token_addresses {
4904        let identities = executor
4905            .deployment_manifest
4906            .tokens
4907            .iter()
4908            .filter(|identity| Address::from_str(&identity.address).ok() == Some(address))
4909            .collect::<Vec<_>>();
4910        anyhow::ensure!(
4911            identities.len() == 1,
4912            "Wallet token {address} does not have exactly one deployment manifest identity"
4913        );
4914        let identity = identities[0];
4915        let token = Token::new(
4916            plan.pool.chain.clone(),
4917            address,
4918            identity.name.clone(),
4919            identity.symbol.clone(),
4920            identity.decimals,
4921        );
4922        let call = ERC20::balanceOfCall {
4923            account: executor.wallet_address,
4924        }
4925        .abi_encode();
4926        let amount = required_verification(
4927            executor
4928                .verification
4929                .verify_decoded_call(
4930                    None,
4931                    &address,
4932                    U256::ZERO,
4933                    &call,
4934                    included.block_number,
4935                    |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4936                )
4937                .await,
4938            "finalized token balance",
4939        )?;
4940        decisions.push(verification_decision(
4941            &amount,
4942            Some(included.block_number),
4943            Some(included.block_number),
4944        ));
4945        token_balances.push(TokenBalance::new(amount.value, token));
4946    }
4947
4948    let mut wallet_balance = WalletBalance::new(token_universe);
4949    let balances = wallet_balance.replace_balances(native_balance, token_balances)?;
4950    Ok(VerifiedWalletRefresh {
4951        wallet_balance,
4952        balances,
4953        decisions,
4954    })
4955}
4956
4957async fn verify_connect_capabilities(
4958    verification: &VerificationCoordinator,
4959    manifest: &BlockchainDeploymentManifest,
4960    wallet: Address,
4961    weth: Address,
4962    block: u64,
4963) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4964    let contract = manifest
4965        .contracts
4966        .first()
4967        .ok_or_else(|| anyhow::anyhow!("Deployment manifest has no capability probe target"))?;
4968    let contract_address = Address::from_str(&contract.address)
4969        .with_context(|| "Deployment manifest capability probe target is invalid")?;
4970    let storage = required_verification(
4971        verification
4972            .verify_storage(&contract_address, &B256::ZERO, block)
4973            .await,
4974        "Blockchain explicit-height storage capability",
4975    )?;
4976
4977    let balance_call = ERC20::balanceOfCall { account: wallet }.abi_encode();
4978    let gas = required_verification(
4979        verification
4980            .verify_gas_estimate(&wallet, &weth, U256::ZERO, &balance_call, block)
4981            .await,
4982        "Blockchain explicit-height gas capability",
4983    )?;
4984
4985    let mut decisions = vec![
4986        verification_decision(&storage, Some(block), Some(block)),
4987        verification_decision(&gas, Some(block), Some(block)),
4988    ];
4989
4990    for pool in &manifest.pools {
4991        let quote_contract = Address::from_str(&pool.quote_contract)
4992            .with_context(|| "Deployment manifest quote capability target is invalid")?;
4993        let token_in = Address::from_str(&pool.token0)
4994            .with_context(|| "Deployment manifest quote input token is invalid")?;
4995        let token_out = Address::from_str(&pool.token1)
4996            .with_context(|| "Deployment manifest quote output token is invalid")?;
4997        let fee = U24::try_from(pool.fee)
4998            .map_err(|_| anyhow::anyhow!("Deployment manifest pool fee is invalid"))?;
4999        let quote = required_verification(
5000            verification
5001                .verify_quote_exact_input_single(
5002                    &quote_contract,
5003                    token_in,
5004                    token_out,
5005                    U256::from(1u64),
5006                    fee,
5007                    block,
5008                )
5009                .await,
5010            "Blockchain explicit-height quote capability",
5011        )?;
5012        decisions.push(verification_decision(&quote, Some(block), Some(block)));
5013    }
5014
5015    let trace = required_verification(
5016        verification.verify_call_trace_capability().await,
5017        "Blockchain call trace capability",
5018    )?;
5019    decisions.push(verification_decision(&trace, None, None));
5020    Ok(decisions)
5021}
5022
5023/// Runs the read-only pre-trade checks for a swap: deployed bytecode at the pool, router,
5024/// and token addresses, and an operator-prepared router allowance and input-token balance
5025/// covering the amount. The shared signing pipeline checks the exact maximum native cost.
5026/// Never wraps or approves.
5027async fn check_swap_preconditions(
5028    plan: &SwapPlan,
5029    block: u64,
5030    executor: &TransactionExecutor,
5031) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
5032    let mut decisions = Vec::new();
5033    let factory_call = UniswapV3RouterState::factoryCall.abi_encode();
5034    let router_factory = required_verification(
5035        executor
5036            .verification
5037            .verify_decoded_call(
5038                None,
5039                &plan.router,
5040                U256::ZERO,
5041                &factory_call,
5042                block,
5043                |result| {
5044                    UniswapV3RouterState::factoryCall::abi_decode_returns(result)
5045                        .map_err(Into::into)
5046                },
5047            )
5048            .await,
5049        "swap router factory",
5050    )?;
5051    anyhow::ensure!(
5052        router_factory.value == plan.factory,
5053        "Router {} reports an unexpected factory",
5054        plan.router,
5055    );
5056    decisions.push(verification_decision(
5057        &router_factory,
5058        Some(block),
5059        Some(block),
5060    ));
5061
5062    let weth_call = UniswapV3RouterState::WETH9Call.abi_encode();
5063    let router_weth = required_verification(
5064        executor
5065            .verification
5066            .verify_decoded_call(
5067                None,
5068                &plan.router,
5069                U256::ZERO,
5070                &weth_call,
5071                block,
5072                |result| {
5073                    UniswapV3RouterState::WETH9Call::abi_decode_returns(result).map_err(Into::into)
5074                },
5075            )
5076            .await,
5077        "swap router wrapped native",
5078    )?;
5079    anyhow::ensure!(
5080        router_weth.value == plan.weth,
5081        "Router {} reports an unexpected wrapped native contract",
5082        plan.router,
5083    );
5084    decisions.push(verification_decision(
5085        &router_weth,
5086        Some(block),
5087        Some(block),
5088    ));
5089
5090    let pool_call = UniswapV3Factory::getPoolCall {
5091        tokenA: plan.token_in,
5092        tokenB: plan.token_out,
5093        fee: plan.fee,
5094    }
5095    .abi_encode();
5096    let registered_pool = required_verification(
5097        executor
5098            .verification
5099            .verify_decoded_call(
5100                None,
5101                &plan.factory,
5102                U256::ZERO,
5103                &pool_call,
5104                block,
5105                |result| {
5106                    UniswapV3Factory::getPoolCall::abi_decode_returns(result).map_err(Into::into)
5107                },
5108            )
5109            .await,
5110        "swap factory pool",
5111    )?;
5112    anyhow::ensure!(
5113        registered_pool.value == plan.pool_address,
5114        "Factory resolves an unexpected pool for the swap token pair and fee"
5115    );
5116    decisions.push(verification_decision(
5117        &registered_pool,
5118        Some(block),
5119        Some(block),
5120    ));
5121
5122    for token in [&plan.pool.token0, &plan.pool.token1] {
5123        let decimals_call = ERC20::decimalsCall.abi_encode();
5124        let decimals = required_verification(
5125            executor
5126                .verification
5127                .verify_decoded_call(
5128                    None,
5129                    &token.address,
5130                    U256::ZERO,
5131                    &decimals_call,
5132                    block,
5133                    |result| ERC20::decimalsCall::abi_decode_returns(result).map_err(Into::into),
5134                )
5135                .await,
5136            "swap token decimals",
5137        )?;
5138        anyhow::ensure!(
5139            decimals.value == token.decimals,
5140            "Token {} reports unexpected decimals",
5141            token.address,
5142        );
5143        decisions.push(verification_decision(&decimals, Some(block), Some(block)));
5144    }
5145
5146    let allowance_call = ERC20::allowanceCall {
5147        owner: executor.wallet_address,
5148        spender: plan.router,
5149    }
5150    .abi_encode();
5151    let allowance = required_verification(
5152        executor
5153            .verification
5154            .verify_decoded_call(
5155                None,
5156                &plan.token_in,
5157                U256::ZERO,
5158                &allowance_call,
5159                block,
5160                |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
5161            )
5162            .await,
5163        "swap input allowance",
5164    )?;
5165
5166    if allowance.value < plan.amount_in {
5167        anyhow::bail!(
5168            "Router allowance {} is below the swap amount {} for input token {}; approve the router explicitly before submitting",
5169            allowance.value,
5170            plan.amount_in,
5171            plan.token_in
5172        );
5173    }
5174    decisions.push(verification_decision(&allowance, Some(block), Some(block)));
5175
5176    let balance_call = ERC20::balanceOfCall {
5177        account: executor.wallet_address,
5178    }
5179    .abi_encode();
5180    let balance = required_verification(
5181        executor
5182            .verification
5183            .verify_decoded_call(
5184                None,
5185                &plan.token_in,
5186                U256::ZERO,
5187                &balance_call,
5188                block,
5189                |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
5190            )
5191            .await,
5192        "swap input balance",
5193    )?;
5194
5195    if balance.value < plan.amount_in {
5196        anyhow::bail!(
5197            "Input token {} balance {} is below the swap amount {}",
5198            plan.token_in,
5199            balance.value,
5200            plan.amount_in
5201        );
5202    }
5203    decisions.push(verification_decision(&balance, Some(block), Some(block)));
5204
5205    Ok(decisions)
5206}
5207
5208/// Converts a base-denominated order quantity into raw token units with exact integer
5209/// scaling by the base token decimals.
5210///
5211/// `Quantity::raw` is scaled to the greater of its declared precision and `FIXED_PRECISION`;
5212/// token amounts are scaled to the token's decimals. Quantities not exactly representable in
5213/// token units are rejected rather than rounded.
5214fn quantity_to_raw_amount(quantity: Quantity, decimals: u8) -> anyhow::Result<U256> {
5215    if quantity.is_zero() {
5216        anyhow::bail!("Order quantity must be positive");
5217    }
5218
5219    let raw = U256::from(quantity.raw);
5220    let raw_precision = quantity.precision.max(FIXED_PRECISION);
5221    if decimals >= raw_precision {
5222        let scale = U256::from(10u64)
5223            .checked_pow(U256::from(decimals - raw_precision))
5224            .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5225        raw.checked_mul(scale).ok_or_else(|| {
5226            anyhow::anyhow!("Order amount overflow scaling quantity to raw token units")
5227        })
5228    } else {
5229        let divisor = U256::from(10u64)
5230            .checked_pow(U256::from(raw_precision - decimals))
5231            .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5232        if !(raw % divisor).is_zero() {
5233            anyhow::bail!(
5234                "Order quantity {quantity} is not exactly representable in {decimals} base token decimals"
5235            );
5236        }
5237        Ok(raw / divisor)
5238    }
5239}
5240
5241fn swap_token_pair(
5242    side: OrderSide,
5243    base: Address,
5244    quote: Address,
5245) -> anyhow::Result<(Address, Address)> {
5246    match side {
5247        OrderSide::Sell => Ok((base, quote)),
5248        OrderSide::Buy => Ok((quote, base)),
5249    }
5250}
5251
5252fn fill_price_from_quote(
5253    last_qty: Quantity,
5254    quote_amount: U256,
5255    quote_currency: Currency,
5256) -> anyhow::Result<Price> {
5257    let quote = Money::from_u256(quote_amount, quote_currency)?;
5258    Price::from_decimal_dp(quote.as_decimal() / last_qty.as_decimal(), FIXED_PRECISION)
5259        .map_err(anyhow::Error::from)
5260}
5261
5262fn raw_amount_to_quantity(amount: U256, decimals: u8) -> anyhow::Result<Quantity> {
5263    if amount.is_zero() {
5264        anyhow::bail!("Executed amount must be positive");
5265    }
5266    let quantity = if decimals >= FIXED_PRECISION {
5267        let scale = U256::from(10u64)
5268            .checked_pow(U256::from(decimals - FIXED_PRECISION))
5269            .ok_or_else(|| anyhow::anyhow!("Executed amount scaling overflow"))?;
5270        Quantity::from_u256(amount / scale, FIXED_PRECISION).map_err(anyhow::Error::from)?
5271    } else {
5272        Quantity::from_u256(amount, decimals).map_err(anyhow::Error::from)?
5273    };
5274
5275    if quantity.is_zero() {
5276        anyhow::bail!(
5277            "Executed amount {amount} is below representable quantity precision {FIXED_PRECISION}"
5278        );
5279    }
5280    Ok(quantity)
5281}
5282
5283/// Extracts the positive output amount from an exact-input swap quote.
5284fn exact_output_amount(quote: &SwapQuote, zero_for_one: bool) -> anyhow::Result<U256> {
5285    let amount = if zero_for_one {
5286        quote.amount1
5287    } else {
5288        quote.amount0
5289    };
5290
5291    if !amount.is_negative() {
5292        anyhow::bail!("Swap quote output amount {amount} is not a positive output");
5293    }
5294    Ok(amount.unsigned_abs())
5295}
5296
5297/// Derives the minimum output accepted for the swap: the quoted output reduced by
5298/// `slippage_bps`, in exact integer arithmetic. Rejects a zero minimum, which would leave
5299/// the swap without slippage protection.
5300fn derive_min_amount_out(quoted_amount_out: U256, slippage_bps: u32) -> anyhow::Result<U256> {
5301    if slippage_bps >= BPS_DENOMINATOR {
5302        anyhow::bail!("Slippage {slippage_bps} bps must be below {BPS_DENOMINATOR}");
5303    }
5304    let min_amount_out = quoted_amount_out
5305        .checked_mul(U256::from(BPS_DENOMINATOR - slippage_bps))
5306        .and_then(|scaled| scaled.checked_div(U256::from(BPS_DENOMINATOR)))
5307        .ok_or_else(|| anyhow::anyhow!("Minimum output derivation overflow"))?;
5308    if min_amount_out.is_zero() {
5309        anyhow::bail!(
5310            "Derived minimum output is zero for quoted output {quoted_amount_out} at {slippage_bps} bps slippage"
5311        );
5312    }
5313    Ok(min_amount_out)
5314}
5315
5316impl BlockchainExecutionClient {
5317    async fn build_execution_verification_migration(
5318        &self,
5319        snapshot: ExecutionVerificationMigrationSnapshot,
5320        finalized: VerifiedBlockHeader,
5321        finalized_headers: &[VerifiedBlockHeader],
5322        nonce_verification: &Verified<u64>,
5323    ) -> anyhow::Result<ExecutionVerificationMigration> {
5324        let next_canonical_nonce = nonce_verification.value;
5325        let mut hashes_by_intent: HashMap<i64, Vec<&ExecutionTransactionHashRow>> = HashMap::new();
5326        for hash in &snapshot.hashes {
5327            hashes_by_intent
5328                .entry(hash.intent_id)
5329                .or_default()
5330                .push(hash);
5331        }
5332        let active_count = snapshot
5333            .intents
5334            .iter()
5335            .filter(|intent| intent.active)
5336            .count();
5337        anyhow::ensure!(
5338            active_count <= 1,
5339            "Retained execution history has multiple active signer owners"
5340        );
5341
5342        let mut nonce_owners = HashMap::new();
5343        let mut records = Vec::with_capacity(snapshot.intents.len());
5344        let finalized_headers = finalized_headers
5345            .iter()
5346            .map(durable_verified_header)
5347            .collect::<Vec<_>>();
5348
5349        for intent in &snapshot.intents {
5350            anyhow::ensure!(
5351                intent.chain_id == self.chain.chain_id
5352                    && intent.wallet_address == self.config.wallet_address,
5353                "Retained execution intent belongs to another signer"
5354            );
5355            let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
5356                anyhow::anyhow!("Retained execution intent has an unsupported purpose")
5357            })?;
5358            let hashes = hashes_by_intent
5359                .get(&intent.id)
5360                .map(Vec::as_slice)
5361                .unwrap_or_default();
5362            let current = hashes
5363                .iter()
5364                .copied()
5365                .filter(|hash| hash.current)
5366                .collect::<Vec<_>>();
5367            anyhow::ensure!(
5368                current.len() <= 1,
5369                "Retained execution intent {} has multiple current hashes",
5370                intent.id
5371            );
5372            let current = current.first().copied();
5373            let mut authenticated = HashMap::new();
5374
5375            for hash in hashes {
5376                if hash.payload_expected {
5377                    let raw = open_execution_payload(
5378                        self.payload_keys
5379                            .as_deref()
5380                            .expect("Postgres execution requires payload keys"),
5381                        self.payload_policy(),
5382                        intent,
5383                        hash,
5384                        "verification migration",
5385                    )?;
5386                    authenticated.insert(hash.id, raw);
5387                } else {
5388                    anyhow::ensure!(
5389                        hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
5390                        "Unowned replacement hash retains signed bytes"
5391                    );
5392                }
5393            }
5394
5395            if let Some(nonce) = intent.nonce {
5396                anyhow::ensure!(
5397                    nonce_owners.insert(nonce, intent.id).is_none(),
5398                    "Retained execution history has duplicate signer nonce ownership"
5399                );
5400            }
5401
5402            let base_decision = verification_decision(
5403                nonce_verification,
5404                Some(finalized.number),
5405                Some(finalized.number),
5406            );
5407
5408            if !intent.active {
5409                if matches!(intent.status.as_str(), "finalized" | "reverted") {
5410                    let expected_marker =
5411                        if purpose == TransactionPurpose::Swap && intent.status == "finalized" {
5412                            intent.fill_emitted
5413                        } else {
5414                            intent.terminal_emitted
5415                        };
5416                    anyhow::ensure!(
5417                        expected_marker,
5418                        "Released terminal intent {} has no durable event marker",
5419                        intent.id
5420                    );
5421                } else {
5422                    anyhow::ensure!(
5423                        matches!(intent.status.as_str(), "dropped" | "recoverable")
5424                            && authenticated.is_empty(),
5425                        "Released nonterminal intent {} retains signed ownership",
5426                        intent.id
5427                    );
5428                    records.push(ExecutionVerificationMigrationRecord {
5429                        intent_id: intent.id,
5430                        nonce: intent.nonce,
5431                        transaction_hash: None,
5432                        terminal_status: None,
5433                        block_number: None,
5434                        block_hash: None,
5435                        receipt_success: None,
5436                        gas_used: None,
5437                        effective_gas_price: None,
5438                        recover_prepared: false,
5439                        decisions: vec![base_decision],
5440                    });
5441                    continue;
5442                }
5443            }
5444
5445            if intent.active && intent.nonce.is_none() {
5446                anyhow::ensure!(
5447                    intent.status == "prepared" && hashes.is_empty(),
5448                    "Unassigned active intent {} is not an unsigned preparation",
5449                    intent.id
5450                );
5451                records.push(ExecutionVerificationMigrationRecord {
5452                    intent_id: intent.id,
5453                    nonce: None,
5454                    transaction_hash: None,
5455                    terminal_status: None,
5456                    block_number: None,
5457                    block_hash: None,
5458                    receipt_success: None,
5459                    gas_used: None,
5460                    effective_gas_price: None,
5461                    recover_prepared: true,
5462                    decisions: vec![base_decision],
5463                });
5464                continue;
5465            }
5466
5467            let nonce = intent.nonce.ok_or_else(|| {
5468                anyhow::anyhow!("Retained signed intent {} has no nonce", intent.id)
5469            })?;
5470            let current = current.ok_or_else(|| {
5471                anyhow::anyhow!("Retained signed intent {} has no current hash", intent.id)
5472            })?;
5473            let raw_transaction = authenticated.get(&current.id).ok_or_else(|| {
5474                anyhow::anyhow!(
5475                    "Retained signed intent {} has no authenticated current payload",
5476                    intent.id
5477                )
5478            })?;
5479
5480            if intent.active && nonce == next_canonical_nonce {
5481                anyhow::ensure!(
5482                    !matches!(intent.status.as_str(), "finalized" | "reverted"),
5483                    "Active terminal intent conflicts with the canonical nonce ledger"
5484                );
5485                records.push(ExecutionVerificationMigrationRecord {
5486                    intent_id: intent.id,
5487                    nonce: Some(nonce),
5488                    transaction_hash: Some(current.transaction_hash.clone()),
5489                    terminal_status: None,
5490                    block_number: None,
5491                    block_hash: None,
5492                    receipt_success: None,
5493                    gas_used: None,
5494                    effective_gas_price: None,
5495                    recover_prepared: false,
5496                    decisions: vec![base_decision],
5497                });
5498                continue;
5499            }
5500            anyhow::ensure!(
5501                nonce < next_canonical_nonce,
5502                "Retained active nonce {nonce} is above canonical nonce {next_canonical_nonce}"
5503            );
5504
5505            let receipt_verification = required_verification(
5506                self.verification
5507                    .verify_receipt(&B256::from_str(&current.transaction_hash).with_context(
5508                        || {
5509                            format!(
5510                                "Retained transaction hash {} is invalid",
5511                                current.transaction_hash
5512                            )
5513                        },
5514                    )?)
5515                    .await,
5516                "migration receipt",
5517            )?;
5518            let receipt = receipt_verification.value.clone();
5519            anyhow::ensure!(
5520                receipt.block_number <= finalized.number,
5521                "Retained terminal receipt is above the verified finalized boundary"
5522            );
5523            let inclusion_verification = required_verification(
5524                self.verification.verify_block(receipt.block_number).await,
5525                "migration inclusion header",
5526            )?;
5527            anyhow::ensure!(
5528                inclusion_verification.value.hash == receipt.block_hash
5529                    && finalized_headers.iter().any(|header| {
5530                        header.number == receipt.block_number
5531                            && header.hash == receipt.block_hash.to_string()
5532                    }),
5533                "Retained terminal receipt is not on the verified finalized ancestry"
5534            );
5535            let tx_hash = B256::from_str(&current.transaction_hash)
5536                .context("Retained transaction hash is invalid")?;
5537            let included = IncludedTransaction {
5538                intent_id: intent.id,
5539                nonce,
5540                tx_hash,
5541                block_number: receipt.block_number,
5542                receipt: receipt.clone(),
5543                finality: StableFinality {
5544                    decisions: Vec::new(),
5545                    inclusion_header: durable_verified_header(&inclusion_verification.value),
5546                    finalized_headers: finalized_headers.clone(),
5547                },
5548            };
5549            let trace_purpose = match purpose {
5550                TransactionPurpose::Wrap => "wrap",
5551                TransactionPurpose::Approve => "approve",
5552                TransactionPurpose::Swap => {
5553                    match self.restore_swap_plan(intent)?.order.order_side() {
5554                        OrderSide::Sell => "swap_sell",
5555                        OrderSide::Buy => "swap_buy",
5556                    }
5557                }
5558            };
5559            let mut decisions = vec![
5560                verification_decision(
5561                    &receipt_verification,
5562                    Some(receipt.block_number),
5563                    Some(receipt.block_number),
5564                ),
5565                verification_decision(
5566                    &inclusion_verification,
5567                    Some(receipt.block_number),
5568                    Some(receipt.block_number),
5569                ),
5570            ];
5571            decisions.extend(
5572                verify_finalized_transaction_identity(
5573                    &included,
5574                    intent,
5575                    nonce,
5576                    raw_transaction,
5577                    &self.verification,
5578                    self.wallet_address,
5579                    self.chain.chain_id,
5580                    &self
5581                        .config
5582                        .verification
5583                        .as_ref()
5584                        .expect("verification config validated")
5585                        .deployment_manifest,
5586                    trace_purpose,
5587                )
5588                .await?,
5589            );
5590            let terminal_status = if receipt.status {
5591                TransactionStatus::Finalized
5592            } else {
5593                TransactionStatus::Reverted
5594            };
5595
5596            if !intent.active {
5597                anyhow::ensure!(
5598                    intent.status == terminal_status.as_str(),
5599                    "Released terminal intent status conflicts with its verified receipt"
5600                );
5601            }
5602            records.push(ExecutionVerificationMigrationRecord {
5603                intent_id: intent.id,
5604                nonce: Some(nonce),
5605                transaction_hash: Some(current.transaction_hash.clone()),
5606                terminal_status: Some(terminal_status),
5607                block_number: Some(receipt.block_number),
5608                block_hash: Some(receipt.block_hash.to_string()),
5609                receipt_success: Some(receipt.status),
5610                gas_used: Some(receipt.gas_used),
5611                effective_gas_price: Some(receipt.effective_gas_price.to_string()),
5612                recover_prepared: false,
5613                decisions,
5614            });
5615        }
5616
5617        Ok(ExecutionVerificationMigration { snapshot, records })
5618    }
5619}
5620
5621#[async_trait(?Send)]
5622impl ExecutionClient for BlockchainExecutionClient {
5623    fn is_connected(&self) -> bool {
5624        self.core.is_connected()
5625    }
5626
5627    fn client_id(&self) -> ClientId {
5628        self.core.client_id
5629    }
5630
5631    fn account_id(&self) -> AccountId {
5632        self.core.account_id
5633    }
5634
5635    fn venue(&self) -> Venue {
5636        self.core.venue
5637    }
5638
5639    fn handles_order_venue(&self, venue: Venue) -> bool {
5640        venue.parse_dex().is_ok_and(|(blockchain, dex_type)| {
5641            blockchain == self.chain.name && dex_type == DexType::UniswapV3
5642        })
5643    }
5644
5645    fn oms_type(&self) -> OmsType {
5646        self.core.oms_type
5647    }
5648
5649    fn get_account(&self) -> Option<AccountAny> {
5650        self.core.cache().account_owned(&self.core.account_id)
5651    }
5652
5653    fn generate_account_state(
5654        &self,
5655        balances: Vec<AccountBalance>,
5656        margins: Vec<MarginBalance>,
5657        reported: bool,
5658        ts_event: UnixNanos,
5659        info: Option<Params>,
5660    ) -> anyhow::Result<()> {
5661        self.emitter
5662            .try_emit_account_state(balances, margins, reported, ts_event, info)
5663    }
5664
5665    fn start(&mut self) -> anyhow::Result<()> {
5666        if self.core.is_started() {
5667            return Ok(());
5668        }
5669
5670        self.emitter.set_sender(get_exec_event_sender());
5671        self.core.set_started();
5672        log::info!(
5673            "Started: client_id={}, account_id={}",
5674            self.core.client_id,
5675            self.core.account_id
5676        );
5677        Ok(())
5678    }
5679
5680    fn stop(&mut self) -> anyhow::Result<()> {
5681        if self.core.is_stopped() {
5682            return Ok(());
5683        }
5684
5685        self.pending_tasks.begin_shutdown();
5686        self.signer = None;
5687        self.core.set_stopped();
5688        self.core.set_disconnected();
5689        log::info!("Stopped: client_id={}", self.core.client_id);
5690        Ok(())
5691    }
5692
5693    fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
5694        let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
5695
5696        if order.is_closed() {
5697            log::warn!("Cannot submit closed order {}", order.client_order_id());
5698            return Ok(());
5699        }
5700
5701        if !self.pending_tasks.is_open() {
5702            self.emitter
5703                .emit_order_denied(&order, "Blockchain execution client is shutting down");
5704            return Ok(());
5705        }
5706
5707        let plan = match self.prepare_swap(&cmd, &order) {
5708            Ok(plan) => plan,
5709            Err(e) => {
5710                self.emitter.emit_order_denied(&order, &e.to_string());
5711                return Ok(());
5712            }
5713        };
5714
5715        let executor = match self.transaction_executor() {
5716            Ok(executor) => executor,
5717            Err(e) => {
5718                self.emitter.emit_order_denied(&order, &e.to_string());
5719                return Ok(());
5720            }
5721        };
5722
5723        let emitter = self.emitter.clone();
5724        let max_quote_age_blocks = self.transaction_limits.max_quote_age_blocks;
5725        let deadline_seconds = self.transaction_limits.deadline_seconds;
5726        let client_order_id = order.client_order_id();
5727
5728        let future = async move {
5729            if let Err(e) = execute_swap(
5730                plan,
5731                executor,
5732                emitter,
5733                max_quote_age_blocks,
5734                deadline_seconds,
5735            )
5736            .await
5737            {
5738                log::warn!("Swap execution for order {client_order_id} failed: {e:?}");
5739            }
5740        };
5741
5742        if let Err(e) = self.pending_tasks.spawn(future) {
5743            release_preparing_slot(&self.in_flight);
5744            log::warn!("Skipping blockchain swap after shutdown began: {e}");
5745        }
5746
5747        Ok(())
5748    }
5749
5750    fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
5751        let orders = self
5752            .core
5753            .cache()
5754            .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
5755
5756        for order in &orders {
5757            if order.is_closed() {
5758                log::warn!("Cannot submit closed order {}", order.client_order_id());
5759                continue;
5760            }
5761
5762            self.emitter
5763                .emit_order_denied(order, ORDER_LIST_UNSUPPORTED);
5764        }
5765
5766        Ok(())
5767    }
5768
5769    fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
5770        let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5771            log::warn!("Cannot modify unknown order {}", cmd.client_order_id);
5772            return Ok(());
5773        };
5774
5775        self.emitter.emit_order_modify_rejected(
5776            &order,
5777            cmd.venue_order_id,
5778            ORDER_MODIFY_UNSUPPORTED,
5779            get_atomic_clock_realtime().get_time_ns(),
5780        );
5781        Ok(())
5782    }
5783
5784    fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
5785        let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5786            log::warn!("Cannot cancel unknown order {}", cmd.client_order_id);
5787            return Ok(());
5788        };
5789
5790        self.emitter.emit_order_cancel_rejected(
5791            &order,
5792            cmd.venue_order_id,
5793            ORDER_CANCEL_UNSUPPORTED,
5794            get_atomic_clock_realtime().get_time_ns(),
5795        );
5796        Ok(())
5797    }
5798
5799    fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
5800        log::warn!(
5801            "Cancel-all for {} is not supported on the blockchain execution client",
5802            cmd.instrument_id
5803        );
5804        Ok(())
5805    }
5806
5807    fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
5808        for cancel in cmd.cancels {
5809            self.cancel_order(cancel)?;
5810        }
5811        Ok(())
5812    }
5813
5814    fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
5815        anyhow::ensure!(
5816            cmd.account_id == self.core.account_id,
5817            "Query account ID {} does not match client account ID {}",
5818            cmd.account_id,
5819            self.core.account_id
5820        );
5821        anyhow::ensure!(self.core.is_started(), "Execution client is not started");
5822
5823        let balances = self.wallet_balance.lock().as_account_balances()?;
5824        self.generate_account_state(
5825            balances,
5826            vec![],
5827            true,
5828            get_atomic_clock_realtime().get_time_ns(),
5829            None,
5830        )
5831    }
5832
5833    fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
5834        log::warn!(
5835            "Order queries are not supported on the blockchain execution client; cannot query {}",
5836            cmd.client_order_id
5837        );
5838        Ok(())
5839    }
5840
5841    async fn connect(&mut self) -> anyhow::Result<()> {
5842        if self.core.is_connected() {
5843            log::warn!("Blockchain execution client already connected");
5844            return Ok(());
5845        }
5846
5847        log::info!(
5848            "Connecting to blockchain execution client on chain {}",
5849            self.chain.name
5850        );
5851
5852        if !self.pending_tasks.is_open() || !self.pending_tasks.is_empty() {
5853            self.pending_tasks.begin_shutdown();
5854            self.pending_tasks
5855                .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
5856                .await
5857                .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
5858            self.signer = None;
5859            self.pending_tasks
5860                .start_generation()
5861                .map_err(|e| anyhow::anyhow!("Failed to start blockchain task generation: {e}"))?;
5862        }
5863        release_preparing_slot(&self.in_flight);
5864
5865        let setup_guard = TaskGroupGuard::new(&[&self.pending_tasks], || {});
5866
5867        let payload_keys = PayloadKeySet::load(
5868            self.config.payload_key_env.as_deref(),
5869            &self.config.payload_key_retired_env,
5870            self.config.payload_deployment_id.as_deref(),
5871        )?
5872        .map(Arc::new);
5873
5874        // Attach or reuse the durable store for execution transaction records
5875        if self.cache.database.is_some() || self.config.postgres_cache_database_config.is_some() {
5876            let keys = payload_keys.as_deref().ok_or_else(|| {
5877                anyhow::anyhow!(
5878                    "Postgres execution requires an active payload key and deployment identity"
5879                )
5880            })?;
5881
5882            if self.cache.database.is_none() {
5883                let pg_options = self
5884                    .config
5885                    .postgres_cache_database_config
5886                    .as_ref()
5887                    .expect("Postgres configuration checked above");
5888                let database = crate::cache::database::BlockchainCacheDatabase::connect(
5889                    pg_options.clone().into(),
5890                )
5891                .await
5892                .map_err(|e| {
5893                    anyhow::anyhow!("Failed to connect to the Postgres cache database: {e}")
5894                })?;
5895                self.cache.database = Some(database);
5896            }
5897            self.cache
5898                .database
5899                .as_ref()
5900                .expect("database was attached")
5901                .require_execution_payload_storage_ready(keys)
5902                .await?;
5903            self.cache.initialize_chain().await;
5904            self.cache.ensure_execution_transaction_schema().await?;
5905            let check = self
5906                .cache
5907                .database
5908                .as_ref()
5909                .expect("database was attached")
5910                .check_execution_payload_storage(
5911                    Some(keys),
5912                    Some(PayloadPolicy {
5913                        chain_id: self.chain.chain_id,
5914                        signer: self.wallet_address,
5915                        gas_limit: self.config.gas_limit,
5916                        max_fee_per_gas: self.config.max_fee_per_gas_wei,
5917                    }),
5918                    100,
5919                )
5920                .await?;
5921            anyhow::ensure!(
5922                check.protected,
5923                "Postgres execution requires protected payload storage"
5924            );
5925        } else {
5926            log::warn!(
5927                "No Postgres cache database configured; transactions will be refused (no durable store)"
5928            );
5929        }
5930        self.payload_keys = payload_keys;
5931
5932        let verification = self
5933            .config
5934            .verification
5935            .as_ref()
5936            .expect("verification config validated at construction");
5937        let position = if let Some(database) = self.cache.database.as_ref() {
5938            database
5939                .load_execution_verification_position(
5940                    self.chain.chain_id,
5941                    &self.config.wallet_address,
5942                    &verification.manifest_version,
5943                    &verification.manifest_digest,
5944                )
5945                .await?
5946        } else {
5947            None
5948        };
5949        let migration_snapshot = if position.is_none() {
5950            if let Some(database) = self.cache.database.as_ref() {
5951                let snapshot = database
5952                    .load_execution_verification_migration_snapshot(
5953                        self.chain.chain_id,
5954                        &self.config.wallet_address,
5955                    )
5956                    .await?;
5957
5958                if snapshot.intents.is_empty() {
5959                    None
5960                } else {
5961                    Some(snapshot)
5962                }
5963            } else {
5964                None
5965            }
5966        } else {
5967            None
5968        };
5969
5970        let chain_id_verification = required_verification(
5971            self.verification.verify_chain_id().await,
5972            "Blockchain chain ID",
5973        )?;
5974        let checkpoint_verification = required_verification(
5975            self.verification.verify_checkpoint().await,
5976            "Blockchain checkpoint",
5977        )?;
5978        let checkpoint = checkpoint_verification.value;
5979        let finalized_verification = required_verification(
5980            self.verification.verify_finalized_header().await,
5981            "Blockchain finalized header",
5982        )?;
5983        let finalized = finalized_verification.value;
5984        let mut connect_decisions = vec![
5985            verification_decision(&chain_id_verification, None, None),
5986            verification_decision(
5987                &checkpoint_verification,
5988                Some(checkpoint.number),
5989                Some(checkpoint.number),
5990            ),
5991            verification_decision(
5992                &finalized_verification,
5993                Some(finalized.number),
5994                Some(finalized.number),
5995            ),
5996        ];
5997        let mut finalized_headers = if let Some(position) = position.as_ref() {
5998            let durable_tip = parse_verified_header(&position.finalized_tip)?;
5999            anyhow::ensure!(
6000                durable_tip.number >= checkpoint.number,
6001                "Durable finalized header tip precedes the trusted checkpoint"
6002            );
6003            let durable_tip_verification = required_verification(
6004                self.verification.verify_block(durable_tip.number).await,
6005                "Blockchain durable finalized tip",
6006            )?;
6007            anyhow::ensure!(
6008                durable_tip_verification.value == durable_tip,
6009                "Durable finalized header tip conflicts with independent sources"
6010            );
6011            connect_decisions.push(verification_decision(
6012                &durable_tip_verification,
6013                Some(durable_tip.number),
6014                Some(durable_tip.number),
6015            ));
6016            vec![durable_tip]
6017        } else {
6018            vec![checkpoint]
6019        };
6020        let mut ancestry_cursor = *finalized_headers
6021            .last()
6022            .expect("finalized header ledger is nonempty");
6023        anyhow::ensure!(
6024            finalized.number >= ancestry_cursor.number,
6025            "Verified finalized height regressed below the durable finalized header tip"
6026        );
6027
6028        while ancestry_cursor.number < finalized.number {
6029            let end = ancestry_cursor
6030                .number
6031                .saturating_add(4_096)
6032                .min(finalized.number);
6033            let start = ancestry_cursor.number.saturating_add(1);
6034            let headers_verification = required_verification(
6035                self.verification
6036                    .verify_header_window(ancestry_cursor, end)
6037                    .await,
6038                "Blockchain finalized ancestry",
6039            )?;
6040            let headers = &headers_verification.value;
6041            ancestry_cursor = *headers
6042                .last()
6043                .expect("nonempty ancestry window advances the cursor");
6044            finalized_headers.extend(headers.iter().copied());
6045            connect_decisions.push(verification_decision(
6046                &headers_verification,
6047                Some(start),
6048                Some(end),
6049            ));
6050        }
6051        anyhow::ensure!(
6052            ancestry_cursor == finalized,
6053            "Verified finalized header conflicts with its ancestry window"
6054        );
6055        let nonce_verification = required_verification(
6056            self.verification
6057                .verify_transaction_count(&self.wallet_address, finalized.number)
6058                .await,
6059            "Blockchain finalized transaction count",
6060        )?;
6061        let observed_canonical_nonce = nonce_verification.value;
6062        let next_canonical_nonce = position
6063            .as_ref()
6064            .map_or(observed_canonical_nonce, |position| {
6065                position.next_canonical_nonce
6066            });
6067
6068        if let Some(position) = position.as_ref() {
6069            log::debug!(
6070                "Resumed execution verification ledger at nonce revision {} with observed finalized nonce {}",
6071                position.revision,
6072                observed_canonical_nonce,
6073            );
6074        }
6075        connect_decisions.push(verification_decision(
6076            &nonce_verification,
6077            Some(finalized.number),
6078            Some(finalized.number),
6079        ));
6080        let deployment_verification = required_verification(
6081            self.verification
6082                .verify_deployment_manifest(&verification.deployment_manifest, finalized.number)
6083                .await,
6084            "Blockchain deployment manifest",
6085        )?;
6086        connect_decisions.push(verification_decision(
6087            &deployment_verification,
6088            Some(finalized.number),
6089            Some(finalized.number),
6090        ));
6091        connect_decisions.extend(
6092            verify_connect_capabilities(
6093                &self.verification,
6094                &verification.deployment_manifest,
6095                self.wallet_address,
6096                self.weth_address,
6097                finalized.number,
6098            )
6099            .await?,
6100        );
6101        let migration = if let Some(snapshot) = migration_snapshot {
6102            Some(
6103                self.build_execution_verification_migration(
6104                    snapshot,
6105                    finalized,
6106                    &finalized_headers,
6107                    &nonce_verification,
6108                )
6109                .await?,
6110            )
6111        } else {
6112            None
6113        };
6114
6115        if let Some(database) = self.cache.database.as_ref() {
6116            let identities = std::iter::once(&verification.authoritative)
6117                .chain(
6118                    verification
6119                        .verifiers
6120                        .iter()
6121                        .map(|provider| &provider.identity),
6122                )
6123                .collect::<Vec<_>>();
6124            let provider_ids = identities
6125                .iter()
6126                .map(|identity| identity.provider_id.clone())
6127                .collect::<Vec<_>>();
6128            let operator_ids = identities
6129                .iter()
6130                .map(|identity| identity.operator_id.clone())
6131                .collect::<Vec<_>>();
6132            let failure_domain_ids = identities
6133                .iter()
6134                .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
6135                .collect::<Vec<_>>();
6136            let finalized_headers = finalized_headers
6137                .iter()
6138                .map(durable_verified_header)
6139                .collect::<Vec<_>>();
6140            database
6141                .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
6142                    chain_id: self.chain.chain_id,
6143                    wallet_address: &self.config.wallet_address,
6144                    manifest_version: &verification.manifest_version,
6145                    manifest_digest: &verification.manifest_digest,
6146                    checkpoint_number: checkpoint.number,
6147                    checkpoint_hash: &checkpoint.hash.to_string(),
6148                    checkpoint_parent_hash: &checkpoint.parent_hash.to_string(),
6149                    checkpoint_timestamp: checkpoint.timestamp,
6150                    checkpoint_base_fee_per_gas: checkpoint.base_fee_per_gas,
6151                    finalized_headers: &finalized_headers,
6152                    next_canonical_nonce,
6153                    observed_canonical_nonce,
6154                    provider_ids: &provider_ids,
6155                    operator_ids: &operator_ids,
6156                    failure_domain_ids: &failure_domain_ids,
6157                    decisions: &connect_decisions,
6158                    migration: migration.as_ref(),
6159                })
6160                .await?;
6161        }
6162
6163        let payload_connect_lease = if let (Some(database), Some(keys)) =
6164            (self.cache.database.as_ref(), self.payload_keys.as_deref())
6165        {
6166            Some(
6167                database
6168                    .require_execution_payload_storage(keys, self.payload_policy(), 100)
6169                    .await?,
6170            )
6171        } else {
6172            None
6173        };
6174
6175        // Load the signer key from the configured environment variable; the key is never
6176        // logged, serialized, or stored in configuration
6177        let private_key = Zeroizing::new(
6178            std::env::var(&self.config.signer_private_key_env).map_err(|_| {
6179                anyhow::anyhow!(
6180                    "Signer private key environment variable '{}' is not set",
6181                    self.config.signer_private_key_env
6182                )
6183            })?,
6184        );
6185        let encoded_key = private_key.trim();
6186        let encoded_key = encoded_key.strip_prefix("0x").unwrap_or(encoded_key);
6187        let key_bytes = Zeroizing::new(hex::decode_array::<32>(encoded_key).map_err(|_| {
6188            anyhow::anyhow!(
6189                "Signer private key in '{}' is not a valid hex private key",
6190                self.config.signer_private_key_env
6191            )
6192        })?);
6193        let signer = PrivateKeySigner::from_slice(&key_bytes[..]).map_err(|_| {
6194            anyhow::anyhow!(
6195                "Signer private key in '{}' is not a valid secp256k1 private key",
6196                self.config.signer_private_key_env
6197            )
6198        })?;
6199
6200        if signer.address() != self.wallet_address {
6201            anyhow::bail!(
6202                "Signer address {} derived from '{}' does not match configured wallet address {}",
6203                signer.address(),
6204                self.config.signer_private_key_env,
6205                self.wallet_address
6206            );
6207        }
6208
6209        self.signer = Some(Arc::new(signer));
6210        drop(payload_connect_lease);
6211
6212        if self.cache.has_database()
6213            && let Err(e) = self.reconcile_unresolved_execution().await
6214        {
6215            self.signer = None;
6216            return Err(e);
6217        }
6218
6219        if let Err(e) = self.refresh_wallet_balances().await {
6220            self.signer = None;
6221            return Err(e);
6222        }
6223        self.core.set_connected();
6224        setup_guard.disarm();
6225        log::info!(
6226            "Blockchain execution client connected on chain {}",
6227            self.chain.name
6228        );
6229        Ok(())
6230    }
6231
6232    async fn disconnect(&mut self) -> anyhow::Result<()> {
6233        self.pending_tasks.begin_shutdown();
6234        let tasks_result = self
6235            .pending_tasks
6236            .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
6237            .await;
6238        self.signer = None;
6239        self.core.set_disconnected();
6240        tasks_result
6241            .map(|_| ())
6242            .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
6243        Ok(())
6244    }
6245
6246    async fn generate_order_status_report(
6247        &self,
6248        _cmd: &GenerateOrderStatusReport,
6249    ) -> anyhow::Result<Option<OrderStatusReport>> {
6250        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6251    }
6252
6253    async fn generate_order_status_reports(
6254        &self,
6255        _cmd: &GenerateOrderStatusReports,
6256    ) -> anyhow::Result<Vec<OrderStatusReport>> {
6257        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6258    }
6259
6260    async fn generate_fill_reports(
6261        &self,
6262        _cmd: GenerateFillReports,
6263    ) -> anyhow::Result<Vec<FillReport>> {
6264        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6265    }
6266
6267    async fn generate_position_status_reports(
6268        &self,
6269        _cmd: &GeneratePositionStatusReports,
6270    ) -> anyhow::Result<Vec<PositionStatusReport>> {
6271        anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6272    }
6273
6274    async fn generate_mass_status(
6275        &self,
6276        _lookback_mins: Option<u64>,
6277    ) -> anyhow::Result<Option<ExecutionMassStatus>> {
6278        // Venue mass status is unsupported; durable intent reconciliation at connect
6279        // covers restart recovery, and Ok(None) keeps LiveNode startup safe
6280        log::warn!(
6281            "Mass status is not supported on the blockchain execution client; skipping venue reconciliation"
6282        );
6283        Ok(None)
6284    }
6285}
6286
6287#[cfg(test)]
6288mod tests {
6289    use std::{
6290        cell::RefCell,
6291        rc::Rc,
6292        sync::atomic::{AtomicU64, Ordering},
6293    };
6294
6295    use alloy::{
6296        primitives::{address, aliases::I24},
6297        sol_types::SolValue,
6298    };
6299    use nautilus_common::{
6300        cache::Cache, live::runner::replace_exec_event_sender, messages::ExecutionEvent,
6301        testing::wait_until_async,
6302    };
6303    use nautilus_core::UUID4;
6304    use nautilus_infrastructure::sql::pg::{PostgresConnectOptions, get_postgres_connect_options};
6305    use nautilus_model::{
6306        defi::{
6307            PoolProfiler,
6308            chain::chains,
6309            data::block::BlockPosition,
6310            pool_analysis::{
6311                position::PoolPosition,
6312                snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
6313            },
6314            tick_map::{tick::PoolTick, tick_math::get_tick_at_sqrt_ratio},
6315        },
6316        enums::AccountType,
6317        events::OrderEventAny,
6318        identifiers::{OrderListId, StrategyId, TraderId},
6319        orders::{OrderList, OrderTestBuilder},
6320        types::Price,
6321    };
6322    use rstest::rstest;
6323    use sqlx::postgres::{PgAdvisoryLock, PgAdvisoryLockKey, PgPoolOptions};
6324
6325    use super::*;
6326    use crate::{
6327        cache::database::tests::connect_test_database,
6328        config::{
6329            BlockchainCallEdgeManifest, BlockchainChainAnchorConfig, BlockchainContractManifest,
6330            BlockchainContractProbe, BlockchainContractRole, BlockchainDeploymentManifest,
6331            BlockchainPoolManifest, BlockchainProviderIdentity, BlockchainTokenManifest,
6332            BlockchainVerificationConfig, BlockchainVerificationProviderConfig, QuoteSpendLimit,
6333        },
6334        constants::BLOCKCHAIN_VENUE,
6335        exchanges::arbitrum::UNISWAP_V3,
6336        rpc::http::{
6337            EXECUTION_RPC_TIMEOUT_SECS,
6338            tests::mock::{MockRpcState, start_mock_rpc_server},
6339        },
6340    };
6341
6342    /// Polls for the receipt of a broadcast transaction until it exists or the poll bound
6343    /// is exhausted. A `null` receipt result is a legitimate pending response.
6344    async fn poll_for_receipt(
6345        http_rpc_client: &BlockchainHttpRpcClient,
6346        tx_hash: &B256,
6347        max_polls: u32,
6348        interval: Duration,
6349    ) -> anyhow::Result<Option<RpcTransactionReceipt>> {
6350        let mut last_error = None;
6351        let mut observed_pending = false;
6352
6353        for attempt in 0..max_polls {
6354            if attempt > 0 {
6355                tokio::time::sleep(interval).await;
6356            }
6357
6358            match http_rpc_client.get_transaction_receipt(tx_hash).await {
6359                Ok(Some(receipt)) => return Ok(Some(receipt)),
6360                Ok(None) => observed_pending = true,
6361                Err(e) => {
6362                    log::warn!(
6363                        "Receipt poll {}/{} for transaction {tx_hash} failed: {e}",
6364                        attempt + 1,
6365                        max_polls
6366                    );
6367                    last_error = Some(e);
6368                }
6369            }
6370        }
6371
6372        if !observed_pending && let Some(e) = last_error {
6373            return Err(e);
6374        }
6375
6376        Ok(None)
6377    }
6378
6379    const CHAIN_ID_ARBITRUM: &str =
6380        include_str!("../../test_data/execution/rpc_eth_chain_id_arbitrum.json");
6381    const CHAIN_ID_ETHEREUM: &str =
6382        include_str!("../../test_data/execution/rpc_eth_chain_id_ethereum.json");
6383    const GET_CODE_DEPLOYED: &str =
6384        include_str!("../../test_data/execution/rpc_eth_get_code_deployed.json");
6385    const GET_CODE_EMPTY: &str =
6386        include_str!("../../test_data/execution/rpc_eth_get_code_empty.json");
6387    const GET_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_get_balance.json");
6388    const GET_BALANCE_ZERO: &str =
6389        include_str!("../../test_data/execution/rpc_eth_get_balance_zero.json");
6390    const GET_BALANCE_INSUFFICIENT: &str =
6391        include_str!("../../test_data/execution/rpc_eth_get_balance_insufficient.json");
6392    const CALL_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_call_balance.json");
6393    const CALL_BALANCE_AFTER_WRAP: &str =
6394        include_str!("../../test_data/execution/rpc_eth_call_balance_after_wrap.json");
6395    const CALL_BALANCE_WETH: &str =
6396        include_str!("../../test_data/execution/rpc_eth_call_balance_weth.json");
6397    const CALL_BALANCE_USDC: &str =
6398        include_str!("../../test_data/execution/rpc_eth_call_balance_usdc.json");
6399    const CALL_BALANCE_WETH_UPDATED: &str =
6400        include_str!("../../test_data/execution/rpc_eth_call_balance_weth_updated.json");
6401    const CALL_BALANCE_USDC_UPDATED: &str =
6402        include_str!("../../test_data/execution/rpc_eth_call_balance_usdc_updated.json");
6403    const CALL_BOOL_TRUE: &str =
6404        include_str!("../../test_data/execution/rpc_eth_call_bool_true.json");
6405    const CALL_EMPTY: &str = include_str!("../../test_data/execution/rpc_eth_call_empty.json");
6406    const CALL_ZERO: &str = include_str!("../../test_data/execution/rpc_eth_call_zero.json");
6407    const CALL_ALLOWANCE: &str =
6408        include_str!("../../test_data/execution/rpc_eth_call_allowance.json");
6409    const CALL_ALLOWANCE_1000: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000003e8\"}";
6410    const CALL_ALLOWANCE_MAX: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"}";
6411    const CALL_FACTORY: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000001f98431c8ad98523631ae4a59f267346ea31f984\"}";
6412    const CALL_WETH: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1\"}";
6413    const CALL_USDC: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831\"}";
6414    const CALL_FEE_500: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000001f4\"}";
6415    const CALL_POOL: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000c6962004f452be9203591991d15f6b388e09e8d0\"}";
6416    const CALL_REVERTED: &str =
6417        r#"{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted"}}"#;
6418    const STORAGE_ZERO: &str = r#"{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000000"}"#;
6419    const TRACE_UNKNOWN_TRANSACTION: &str =
6420        r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"transaction not found"}}"#;
6421    const CALL_DECIMALS_18: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000012\"}";
6422    const CALL_DECIMALS_6: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000006\"}";
6423    const TRANSACTION_COUNT: &str =
6424        include_str!("../../test_data/execution/rpc_eth_get_transaction_count.json");
6425    const TRANSACTION_COUNT_NEXT: &str =
6426        include_str!("../../test_data/execution/rpc_eth_get_transaction_count_next.json");
6427    const ESTIMATE_GAS: &str = include_str!("../../test_data/execution/rpc_eth_estimate_gas.json");
6428    const MAX_PRIORITY_FEE: &str =
6429        include_str!("../../test_data/execution/rpc_eth_max_priority_fee_per_gas.json");
6430    const BLOCK_BY_NUMBER: &str =
6431        include_str!("../../test_data/execution/rpc_eth_get_block_by_number.json");
6432    const BLOCK_CANONICAL: &str =
6433        include_str!("../../test_data/execution/rpc_eth_get_block_canonical.json");
6434    const BLOCK_FINALIZED: &str =
6435        include_str!("../../test_data/execution/rpc_eth_get_block_finalized.json");
6436    const RECEIPT_SUCCESS: &str =
6437        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_success.json");
6438    const RECEIPT_REVERTED: &str =
6439        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_reverted.json");
6440    const RECEIPT_NULL: &str =
6441        include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_null.json");
6442    const SEND_RAW_TRANSACTION: &str =
6443        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction.json");
6444    const SEND_RAW_TRANSACTION_REJECTED: &str =
6445        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_rejected.json");
6446    const SEND_RAW_TRANSACTION_NONCE_TOO_LOW: &str =
6447        include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_nonce_too_low.json");
6448    const RPC_METHOD_NOT_FOUND: &str =
6449        include_str!("../../test_data/execution/rpc_error_method_not_found.json");
6450
6451    const WALLET: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
6452    const ROUTER: &str = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
6453    const WETH: &str = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1";
6454    const USDC: &str = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
6455    const LIVE_READ_SMOKE_ENV: &str = "BLOCKCHAIN_LIVE_READ_SMOKE";
6456    const LIVE_READ_SMOKE_RPC: &str = "https://arb1.arbitrum.io/rpc";
6457    const TEST_TIMEOUT: Duration = Duration::from_secs(10);
6458
6459    const WETH_ADDRESS: Address = address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1");
6460    const USDC_ADDRESS: Address = address!("af88d065e77c8cC2239327C5EDb3A432268e5831");
6461    const ROUTER_ADDRESS: Address = address!("E592427A0AEce92De3Edee1F18E0157C05861564");
6462
6463    // Anvil development key for WALLET (public, test-only)
6464    const TEST_PRIVATE_KEY: &str =
6465        "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
6466
6467    const BALANCE_OF_SELECTOR: &str = "0x70a08231";
6468    const ALLOWANCE_SELECTOR: &str = "0xdd62ed3e";
6469    const POOL_TOKEN0_SELECTOR: &str = "0x0dfe1681";
6470    const POOL_TOKEN1_SELECTOR: &str = "0xd21220a7";
6471    const POOL_FEE_SELECTOR: &str = "0xddca3f43";
6472    const DECIMALS_SELECTOR: &str = "0x313ce567";
6473    const FACTORY_SELECTOR: &str = "0xc45a0155";
6474    const WETH9_SELECTOR: &str = "0x4aa4a4fc";
6475    const GET_POOL_SELECTOR: &str = "0x1698ee82";
6476    const QUOTE_EXACT_INPUT_SELECTOR: &str = "0xc6a5026a";
6477    const QUOTE_EXACT_OUTPUT_SELECTOR: &str = "0xbd21704a";
6478
6479    fn test_pool() -> Pool {
6480        let chain = Arc::new(chains::ARBITRUM.clone());
6481        let dex = UNISWAP_V3.dex.clone();
6482        let weth = Token::new(
6483            chain.clone(),
6484            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
6485            "Wrapped Ether".to_string(),
6486            "WETH".to_string(),
6487            18,
6488        );
6489        let usdc = Token::new(
6490            chain.clone(),
6491            address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
6492            "USD Coin".to_string(),
6493            "USDC".to_string(),
6494            6,
6495        );
6496
6497        Pool::new(
6498            chain,
6499            dex,
6500            address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
6501            PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
6502            55_000_000,
6503            weth,
6504            usdc,
6505            Some(500),
6506            Some(10),
6507            UnixNanos::default(),
6508        )
6509    }
6510
6511    fn test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6512        test_config_with_signer_env(http_rpc_url, "BLOCKCHAIN_TEST_PRIVATE_KEY")
6513    }
6514
6515    fn test_config_with_signer_env(
6516        http_rpc_url: String,
6517        signer_env: &str,
6518    ) -> BlockchainExecutionClientConfig {
6519        let verifier_separator = if http_rpc_url.contains('?') { '&' } else { '?' };
6520        let code_hash = keccak256(
6521            hex::decode("6080604052348015600e575f5ffd5b5060").expect("valid test bytecode"),
6522        )
6523        .to_string();
6524        let result = |response: &str| {
6525            serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
6526                .as_str()
6527                .unwrap()
6528                .to_string()
6529        };
6530        let probe = |call_data: &str, expected_output: String| BlockchainContractProbe {
6531            call_data: call_data.to_string(),
6532            expected_output,
6533        };
6534        let contract = |address: &str, role| {
6535            let probes = match role {
6536                BlockchainContractRole::Router => vec![
6537                    probe(FACTORY_SELECTOR, result(CALL_FACTORY)),
6538                    probe(WETH9_SELECTOR, result(CALL_WETH)),
6539                ],
6540                BlockchainContractRole::Factory => vec![probe(
6541                    &hex::encode_prefixed(
6542                        UniswapV3Factory::getPoolCall {
6543                            tokenA: WETH_ADDRESS,
6544                            tokenB: USDC_ADDRESS,
6545                            fee: U24::try_from(500u32).unwrap(),
6546                        }
6547                        .abi_encode(),
6548                    ),
6549                    result(CALL_POOL),
6550                )],
6551                BlockchainContractRole::WrappedNative => {
6552                    vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_18))]
6553                }
6554                BlockchainContractRole::Quote => {
6555                    vec![probe(FACTORY_SELECTOR, result(CALL_FACTORY))]
6556                }
6557                BlockchainContractRole::Token => {
6558                    vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_6))]
6559                }
6560                BlockchainContractRole::Pool => vec![
6561                    probe(POOL_TOKEN0_SELECTOR, result(CALL_WETH)),
6562                    probe(POOL_TOKEN1_SELECTOR, result(CALL_USDC)),
6563                    probe(POOL_FEE_SELECTOR, result(CALL_FEE_500)),
6564                ],
6565                BlockchainContractRole::Implementation => Vec::new(),
6566            };
6567            BlockchainContractManifest {
6568                address: address.to_string(),
6569                role,
6570                runtime_code_hash: code_hash.clone(),
6571                proxy: None,
6572                probes,
6573            }
6574        };
6575        let deployment_manifest = BlockchainDeploymentManifest {
6576            version: "test-v1".to_string(),
6577            chain_id: chains::ARBITRUM.chain_id,
6578            chain_name: chains::ARBITRUM.name.to_string(),
6579            contracts: vec![
6580                contract(ROUTER, BlockchainContractRole::Router),
6581                contract(
6582                    "0x1F98431c8aD98523631AE4a59f267346ea31F984",
6583                    BlockchainContractRole::Factory,
6584                ),
6585                contract(WETH, BlockchainContractRole::WrappedNative),
6586                contract(
6587                    "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
6588                    BlockchainContractRole::Quote,
6589                ),
6590                contract(USDC, BlockchainContractRole::Token),
6591                contract(
6592                    "0xC6962004f452bE9203591991D15f6b388e09E8D0",
6593                    BlockchainContractRole::Pool,
6594                ),
6595            ],
6596            tokens: vec![
6597                BlockchainTokenManifest {
6598                    address: WETH.to_string(),
6599                    name: "Wrapped Ether".to_string(),
6600                    symbol: "WETH".to_string(),
6601                    decimals: 18,
6602                    asset_role: "both".to_string(),
6603                },
6604                BlockchainTokenManifest {
6605                    address: USDC.to_string(),
6606                    name: "USD Coin".to_string(),
6607                    symbol: "USDC".to_string(),
6608                    decimals: 6,
6609                    asset_role: "both".to_string(),
6610                },
6611            ],
6612            pools: vec![BlockchainPoolManifest {
6613                address: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6614                token0: WETH.to_string(),
6615                token1: USDC.to_string(),
6616                fee: 500,
6617                factory: "0x1F98431c8aD98523631AE4a59f267346ea31F984".to_string(),
6618                quote_contract: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e".to_string(),
6619            }],
6620            call_edges: ["swap_sell", "swap_buy"]
6621                .into_iter()
6622                .map(|purpose| BlockchainCallEdgeManifest {
6623                    purpose: purpose.to_string(),
6624                    caller: ROUTER.to_string(),
6625                    target: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6626                    call_type: "call".to_string(),
6627                })
6628                .collect(),
6629        };
6630        let manifest_digest =
6631            keccak256(serde_json::to_vec(&deployment_manifest).unwrap()).to_string();
6632        let verification = BlockchainVerificationConfig {
6633            authoritative: BlockchainProviderIdentity {
6634                provider_id: "authoritative".to_string(),
6635                operator_id: "operator-a".to_string(),
6636                failure_domain_ids: vec!["domain-a".to_string()],
6637            },
6638            verifiers: vec![
6639                BlockchainVerificationProviderConfig {
6640                    identity: BlockchainProviderIdentity {
6641                        provider_id: "verifier-a".to_string(),
6642                        operator_id: "operator-b".to_string(),
6643                        failure_domain_ids: vec!["domain-b".to_string()],
6644                    },
6645                    http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-a"),
6646                },
6647                BlockchainVerificationProviderConfig {
6648                    identity: BlockchainProviderIdentity {
6649                        provider_id: "verifier-b".to_string(),
6650                        operator_id: "operator-c".to_string(),
6651                        failure_domain_ids: vec!["domain-c".to_string()],
6652                    },
6653                    http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-b"),
6654                },
6655            ],
6656            chain_anchor: BlockchainChainAnchorConfig {
6657                chain_id: chains::ARBITRUM.chain_id,
6658                chain_name: chains::ARBITRUM.name.to_string(),
6659                checkpoint_height: 30_346_560,
6660                checkpoint_hash:
6661                    "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
6662                checkpoint_timestamp: 1_761_888_800,
6663                max_head_skew_blocks: 3,
6664                max_head_age_secs: u64::MAX,
6665                max_future_drift_secs: u64::MAX,
6666            },
6667            manifest_version: "test-v1".to_string(),
6668            manifest_digest,
6669            deployment_manifest,
6670        };
6671        BlockchainExecutionClientConfig::builder()
6672            .client_id(AccountId::from("BLOCKCHAIN-001"))
6673            .chain(chains::ARBITRUM.clone())
6674            .wallet_address(WALLET.to_string())
6675            .http_rpc_url(http_rpc_url)
6676            .verification(verification)
6677            .signer_private_key_env(signer_env.to_string())
6678            .router_addresses(vec![ROUTER.to_string()])
6679            .weth_address(WETH.to_string())
6680            .max_fee_per_gas_wei(1_000_000_000)
6681            .base_fee_buffer_bps(2_000)
6682            .gas_limit(1_000_000)
6683            .gas_buffer_bps(2_000)
6684            .allowed_token_pairs(vec![(WETH.to_string(), USDC.to_string())])
6685            .slippage_bps(50)
6686            .max_slippage_bps(200)
6687            .max_order_amount(1_000_000_000_000_000_000)
6688            .deadline_seconds(300)
6689            .max_quote_age_blocks(100)
6690            .receipt_timeout_secs(1)
6691            .build()
6692    }
6693
6694    fn buy_test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6695        let max_amount = expected_buy_amount_in().to_string();
6696        let mut config = test_config(http_rpc_url);
6697        config.allowed_token_pairs = Some(vec![
6698            (WETH.to_string(), USDC.to_string()),
6699            (USDC.to_string(), WETH.to_string()),
6700        ]);
6701        config.quote_spend_limits = Some(vec![quote_spend_limit(USDC, WETH, 6, &max_amount)]);
6702        config
6703    }
6704
6705    fn refresh_test_manifest_digest(config: &mut BlockchainExecutionClientConfig) {
6706        let verification = config.verification.as_mut().unwrap();
6707        verification.manifest_digest =
6708            keccak256(serde_json::to_vec(&verification.deployment_manifest).unwrap()).to_string();
6709    }
6710
6711    fn quote_spend_limit(
6712        token_in: &str,
6713        token_out: &str,
6714        spend_token_decimals: u8,
6715        max_amount: &str,
6716    ) -> QuoteSpendLimit {
6717        QuoteSpendLimit::builder()
6718            .token_in(token_in.to_string())
6719            .token_out(token_out.to_string())
6720            .spend_token(token_in.to_string())
6721            .spend_token_decimals(spend_token_decimals)
6722            .max_amount(max_amount.to_string())
6723            .build()
6724    }
6725
6726    fn test_client_from_config(
6727        config: BlockchainExecutionClientConfig,
6728        pool: Pool,
6729    ) -> BlockchainExecutionClient {
6730        test_client_result(config, pool).unwrap()
6731    }
6732
6733    fn test_client_result(
6734        config: BlockchainExecutionClientConfig,
6735        pool: Pool,
6736    ) -> anyhow::Result<BlockchainExecutionClient> {
6737        test_client_and_cache(config, pool).map(|(client, _)| client)
6738    }
6739
6740    fn test_client_and_cache(
6741        config: BlockchainExecutionClientConfig,
6742        pool: Pool,
6743    ) -> anyhow::Result<(BlockchainExecutionClient, Rc<RefCell<Cache>>)> {
6744        let cache = Rc::new(RefCell::new(Cache::default()));
6745        cache.borrow_mut().add_pool(pool).unwrap();
6746        let core = ExecutionClientCore::new(
6747            TraderId::from("TRADER-001"),
6748            ClientId::from("BLOCKCHAIN-001"),
6749            *BLOCKCHAIN_VENUE,
6750            OmsType::Netting,
6751            AccountId::from("BLOCKCHAIN-001"),
6752            AccountType::Wallet,
6753            None,
6754            cache.clone(),
6755        );
6756
6757        let client = BlockchainExecutionClient::new(core, config)?;
6758        Ok((client, cache))
6759    }
6760
6761    fn test_client(http_rpc_url: String) -> BlockchainExecutionClient {
6762        test_client_from_config(test_config(http_rpc_url), test_pool())
6763    }
6764
6765    async fn client_with_mock_rpc(
6766        state: MockRpcState,
6767    ) -> (BlockchainExecutionClient, MockRpcState) {
6768        let addr = start_mock_rpc_server(state.clone()).await;
6769        (test_client(format!("http://{addr}")), state)
6770    }
6771
6772    async fn client_with_token_mock_rpc(
6773        state: MockRpcState,
6774        signer_env: &str,
6775    ) -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
6776        let state = with_connect_capabilities(state);
6777        let addr = start_mock_rpc_server(state.clone()).await;
6778        let pool = test_pool();
6779        let tokens = [pool.token0.clone(), pool.token1.clone()];
6780        let mut config = test_config_with_signer_env(format!("http://{addr}"), signer_env);
6781        config.tokens = Some(vec![WETH.to_string(), USDC.to_string()]);
6782        let (mut client, cache) = test_client_and_cache(config, pool).unwrap();
6783        for token in tokens {
6784            client.cache.add_token(token).await.unwrap();
6785        }
6786        (client, state, cache)
6787    }
6788
6789    fn execution_rpc_state() -> MockRpcState {
6790        MockRpcState::default()
6791            .with_receipt_hash_from_request()
6792            .with_response("eth_chainId", CHAIN_ID_ARBITRUM)
6793            .with_response("eth_getCode", GET_CODE_DEPLOYED)
6794            .with_response("eth_getBalance", GET_BALANCE)
6795            .with_response("eth_getBlockByNumber", BLOCK_BY_NUMBER)
6796            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6797            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", BLOCK_CANONICAL)
6798            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_FINALIZED)
6799            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d42", BLOCK_FINALIZED)
6800            .with_response("eth_maxPriorityFeePerGas", MAX_PRIORITY_FEE)
6801            .with_call_response(FACTORY_SELECTOR, CALL_FACTORY)
6802            .with_call_response(WETH9_SELECTOR, CALL_WETH)
6803            .with_call_response(GET_POOL_SELECTOR, CALL_POOL)
6804            .with_call_response(POOL_TOKEN0_SELECTOR, CALL_WETH)
6805            .with_call_response(POOL_TOKEN1_SELECTOR, CALL_USDC)
6806            .with_call_response(POOL_FEE_SELECTOR, CALL_FEE_500)
6807            .with_contract_call_response(WETH, DECIMALS_SELECTOR, CALL_DECIMALS_18)
6808            .with_contract_call_response(USDC, DECIMALS_SELECTOR, CALL_DECIMALS_6)
6809            .with_call_response(
6810                QUOTE_EXACT_INPUT_SELECTOR,
6811                &quote_response(expected_sell_quote_amount()),
6812            )
6813            .with_call_response(
6814                QUOTE_EXACT_OUTPUT_SELECTOR,
6815                &quote_response(expected_buy_amount_in()),
6816            )
6817    }
6818
6819    fn ready_rpc_state() -> MockRpcState {
6820        with_connect_capabilities(execution_rpc_state())
6821            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
6822            .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE)
6823    }
6824
6825    fn with_connect_capabilities(state: MockRpcState) -> MockRpcState {
6826        state
6827            .with_response("eth_getStorageAt", STORAGE_ZERO)
6828            .with_response("eth_estimateGas", ESTIMATE_GAS)
6829            .with_parameter_response(
6830                "debug_traceTransaction",
6831                &B256::ZERO.to_string(),
6832                TRACE_UNKNOWN_TRANSACTION,
6833            )
6834    }
6835
6836    fn signing_rpc_state() -> MockRpcState {
6837        ready_rpc_state()
6838            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6839            .with_response("eth_estimateGas", ESTIMATE_GAS)
6840    }
6841
6842    fn broadcast_rpc_state() -> MockRpcState {
6843        execution_rpc_state()
6844            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6845            .with_response("eth_estimateGas", ESTIMATE_GAS)
6846            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
6847            .with_send_raw_transaction_echo()
6848            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO, CALL_ALLOWANCE_1000])
6849    }
6850
6851    async fn expected_wrap_tx_hash(value: U256) -> B256 {
6852        expected_tx_hash(
6853            WETH_ADDRESS,
6854            value,
6855            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
6856        )
6857        .await
6858    }
6859
6860    async fn expected_approve_tx_hash(amount: U256) -> B256 {
6861        let calldata = ERC20::approveCall {
6862            spender: ROUTER_ADDRESS,
6863            amount,
6864        }
6865        .abi_encode();
6866        expected_tx_hash(WETH_ADDRESS, U256::ZERO, Bytes::from(calldata)).await
6867    }
6868
6869    async fn expected_tx_hash(to: Address, value: U256, input: Bytes) -> B256 {
6870        // The orchestration's policy math is deterministic: nonce 7 from the fixture,
6871        // buffered gas 78000, buffered max fee 130000000, priority fee 10000000
6872        let expected_tx =
6873            build_eip1559_transaction(42161, 7, 78_000, 130_000_000, 10_000_000, to, value, input);
6874        let (expected_hash, _) = sign_eip1559_transaction(
6875            expected_tx,
6876            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
6877        )
6878        .await
6879        .unwrap();
6880        expected_hash
6881    }
6882
6883    async fn await_recorded_requests(state: &MockRpcState, method: &str, expected: usize) {
6884        wait_until_async(
6885            || async {
6886                state
6887                    .recorded_requests()
6888                    .iter()
6889                    .filter(|request| request["method"] == method)
6890                    .count()
6891                    >= expected
6892            },
6893            TEST_TIMEOUT,
6894        )
6895        .await;
6896    }
6897
6898    /// The block number served by the `eth_getBlockByNumber` fixture; swap quotes pin their
6899    /// profiler state to it so the freshness check passes.
6900    const FIXTURE_BLOCK: u64 = 30_346_560;
6901    const FIXTURE_BLOCK_PARAM: &str = "0x1cf0d40";
6902    const FIXTURE_BLOCK_HASH: &str =
6903        "0x1111111111111111111111111111111111111111111111111111111111111111";
6904    /// The timestamp served by the `eth_getBlockByNumber` fixture.
6905    const FIXTURE_BLOCK_TIMESTAMP: u64 = 1_761_888_800;
6906    /// Synthetic full-range liquidity for the test pool profiler.
6907    const TEST_LIQUIDITY: u128 = 1_000_000_000_000_000_000_000;
6908
6909    fn test_profiler(pool: &Pool, block_number: u64) -> PoolProfiler {
6910        test_profiler_at_block(pool, block_number, FIXTURE_BLOCK_HASH)
6911    }
6912
6913    fn test_profiler_at_block(pool: &Pool, block_number: u64, block_hash: &str) -> PoolProfiler {
6914        test_profiler_with_range(
6915            pool,
6916            block_number,
6917            block_hash,
6918            U160::from(1u128 << 96),
6919            -887_220,
6920            887_220,
6921            TEST_LIQUIDITY,
6922        )
6923    }
6924
6925    fn test_profiler_with_state(
6926        pool: &Pool,
6927        block_number: u64,
6928        sqrt_price_x96: U160,
6929        liquidity: u128,
6930    ) -> PoolProfiler {
6931        test_profiler_with_range(
6932            pool,
6933            block_number,
6934            FIXTURE_BLOCK_HASH,
6935            sqrt_price_x96,
6936            -887_220,
6937            887_220,
6938            liquidity,
6939        )
6940    }
6941
6942    /// Builds a profiler from a synthetic snapshot with a single position over
6943    /// `[tick_lower, tick_upper]`: exact-input quotes within the range match the
6944    /// constant-product curve, and a tiny test swap never crosses a tick.
6945    fn test_profiler_with_range(
6946        pool: &Pool,
6947        block_number: u64,
6948        block_hash: &str,
6949        sqrt_price_x96: U160,
6950        tick_lower: i32,
6951        tick_upper: i32,
6952        liquidity: u128,
6953    ) -> PoolProfiler {
6954        let snapshot = PoolSnapshot::new(
6955            pool.instrument_id,
6956            PoolState {
6957                current_tick: get_tick_at_sqrt_ratio(sqrt_price_x96),
6958                price_sqrt_ratio_x96: sqrt_price_x96,
6959                liquidity,
6960                protocol_fees_token0: U256::ZERO,
6961                protocol_fees_token1: U256::ZERO,
6962                fee_protocol: 0,
6963                fee_protocol0_basis_points: None,
6964                fee_protocol1_basis_points: None,
6965                fee_growth_global_0: U256::ZERO,
6966                fee_growth_global_1: U256::ZERO,
6967            },
6968            vec![PoolPosition::new(
6969                address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"),
6970                tick_lower,
6971                tick_upper,
6972                liquidity as i128,
6973            )],
6974            vec![
6975                PoolTick::new(
6976                    tick_lower,
6977                    liquidity,
6978                    liquidity as i128,
6979                    U256::ZERO,
6980                    U256::ZERO,
6981                    true,
6982                    0,
6983                ),
6984                PoolTick::new(
6985                    tick_upper,
6986                    liquidity,
6987                    -(liquidity as i128),
6988                    U256::ZERO,
6989                    U256::ZERO,
6990                    true,
6991                    0,
6992                ),
6993            ],
6994            PoolAnalytics::default(),
6995            BlockPosition::new(
6996                block_number,
6997                block_hash.to_string(),
6998                BLOCK_SCOPED_SNAPSHOT_INDEX,
6999                BLOCK_SCOPED_SNAPSHOT_INDEX,
7000            )
7001            .with_block_hash(Some(block_hash.to_string())),
7002            UnixNanos::default(),
7003            UnixNanos::default(),
7004        );
7005        let mut profiler = PoolProfiler::new(Arc::new(pool.clone()));
7006        profiler.restore_from_snapshot(snapshot).unwrap();
7007        profiler
7008    }
7009
7010    fn test_market_sell_order(instrument_id: InstrumentId) -> OrderAny {
7011        market_sell_order_with_id(instrument_id, "O-SWAP-001")
7012    }
7013
7014    fn test_market_buy_order(instrument_id: InstrumentId) -> OrderAny {
7015        market_buy_order_with_id(instrument_id, "O-SWAP-BUY-001")
7016    }
7017
7018    fn submit_order_cmd(order: &OrderAny) -> SubmitOrder {
7019        SubmitOrder::new(
7020            TraderId::from("TRADER-001"),
7021            Some(ClientId::from("BLOCKCHAIN-001")),
7022            order.strategy_id(),
7023            order.instrument_id(),
7024            order.client_order_id(),
7025            order.init_event().clone(),
7026            None,
7027            None,
7028            None,
7029            UUID4::new(),
7030            UnixNanos::default(),
7031            None,
7032        )
7033    }
7034
7035    /// Builds a client whose shared cache holds the test pool, a live profiler at
7036    /// `FIXTURE_BLOCK`, and a SELL market order, without a durable store.
7037    fn swap_client_with_cache(
7038        config: BlockchainExecutionClientConfig,
7039    ) -> (BlockchainExecutionClient, Rc<RefCell<Cache>>) {
7040        let cache = Rc::new(RefCell::new(Cache::default()));
7041        let pool = test_pool();
7042        cache.borrow_mut().add_pool(pool.clone()).unwrap();
7043        cache
7044            .borrow_mut()
7045            .add_order(
7046                test_market_sell_order(pool.instrument_id),
7047                None,
7048                None,
7049                false,
7050            )
7051            .unwrap();
7052        cache
7053            .borrow_mut()
7054            .add_pool_profiler(test_profiler_at_block(
7055                &pool,
7056                FIXTURE_BLOCK,
7057                FIXTURE_BLOCK_HASH,
7058            ))
7059            .unwrap();
7060        let core = ExecutionClientCore::new(
7061            TraderId::from("TRADER-001"),
7062            ClientId::from("BLOCKCHAIN-001"),
7063            *BLOCKCHAIN_VENUE,
7064            OmsType::Netting,
7065            AccountId::from("BLOCKCHAIN-001"),
7066            AccountType::Wallet,
7067            None,
7068            cache.clone(),
7069        );
7070
7071        let client = BlockchainExecutionClient::new(core, config).unwrap();
7072        (client, cache)
7073    }
7074
7075    /// Builds a connected, signer-equipped swap client backed by a fresh Postgres schema.
7076    async fn swap_client_with_database(
7077        test_name: &str,
7078        state: MockRpcState,
7079    ) -> Option<(
7080        sqlx::PgPool,
7081        String,
7082        BlockchainExecutionClient,
7083        MockRpcState,
7084        Rc<RefCell<Cache>>,
7085    )> {
7086        swap_client_with_database_config(test_name, state, test_config).await
7087    }
7088
7089    async fn swap_client_with_buy_database(
7090        test_name: &str,
7091        state: MockRpcState,
7092    ) -> Option<(
7093        sqlx::PgPool,
7094        String,
7095        BlockchainExecutionClient,
7096        MockRpcState,
7097        Rc<RefCell<Cache>>,
7098    )> {
7099        swap_client_with_database_config(test_name, state, buy_test_config).await
7100    }
7101
7102    async fn swap_client_with_database_config<F>(
7103        test_name: &str,
7104        state: MockRpcState,
7105        config: F,
7106    ) -> Option<(
7107        sqlx::PgPool,
7108        String,
7109        BlockchainExecutionClient,
7110        MockRpcState,
7111        Rc<RefCell<Cache>>,
7112    )>
7113    where
7114        F: FnOnce(String) -> BlockchainExecutionClientConfig,
7115    {
7116        let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
7117        let schema = format!("{test_name}_{}", std::process::id());
7118        setup_execution_schema(&admin_pool, &schema).await;
7119
7120        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
7121        let db_options = db_options.options([("search_path", schema.clone())]);
7122        let database = connect_test_database(db_options).await.unwrap();
7123        let addr = start_mock_rpc_server(state.clone()).await;
7124        let (mut client, cache) = swap_client_with_cache(config(format!("http://{addr}")));
7125        client.cache.database = Some(database);
7126        // Mirror the connect-time migration: tests create the pre-submission table shape
7127        client
7128            .cache
7129            .ensure_execution_transaction_schema()
7130            .await
7131            .unwrap();
7132        protect_test_storage(&mut client, &schema).await;
7133        initialize_test_verification_ledger(&client).await;
7134        client.signer = Some(Arc::new(
7135            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7136        ));
7137        client.core.set_connected();
7138
7139        Some((admin_pool, schema, client, state, cache))
7140    }
7141
7142    async fn initialize_test_verification_ledger(client: &BlockchainExecutionClient) {
7143        initialize_test_verification_ledger_with_headers(
7144            client,
7145            &[ExecutionVerifiedHeader {
7146                number: FIXTURE_BLOCK,
7147                hash: FIXTURE_BLOCK_HASH.to_string(),
7148                parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
7149                    .to_string(),
7150                timestamp: FIXTURE_BLOCK_TIMESTAMP,
7151                base_fee_per_gas: Some(100_000_000),
7152            }],
7153        )
7154        .await;
7155    }
7156
7157    async fn initialize_test_verification_ledger_with_headers(
7158        client: &BlockchainExecutionClient,
7159        finalized_headers: &[ExecutionVerifiedHeader],
7160    ) {
7161        ensure_test_verification_ledger(client, finalized_headers, 7, 7)
7162            .await
7163            .unwrap();
7164    }
7165
7166    async fn ensure_test_verification_ledger(
7167        client: &BlockchainExecutionClient,
7168        finalized_headers: &[ExecutionVerifiedHeader],
7169        next_canonical_nonce: u64,
7170        observed_canonical_nonce: u64,
7171    ) -> anyhow::Result<()> {
7172        let verification = client.config.verification.as_ref().unwrap();
7173        let provider_ids = vec![
7174            "authoritative".to_string(),
7175            "verifier-a".to_string(),
7176            "verifier-b".to_string(),
7177        ];
7178        let operator_ids = vec![
7179            "operator-a".to_string(),
7180            "operator-b".to_string(),
7181            "operator-c".to_string(),
7182        ];
7183        let failure_domain_ids = vec![
7184            "domain-a".to_string(),
7185            "domain-b".to_string(),
7186            "domain-c".to_string(),
7187        ];
7188        let decisions = [ExecutionVerificationDecision {
7189            read_class: "numbered_block",
7190            height_start: Some(FIXTURE_BLOCK),
7191            height_end: Some(FIXTURE_BLOCK),
7192            normalized_value_digest: B256::ZERO.to_string(),
7193        }];
7194        client
7195            .cache
7196            .database
7197            .as_ref()
7198            .unwrap()
7199            .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7200                chain_id: 42_161,
7201                wallet_address: WALLET,
7202                manifest_version: &verification.manifest_version,
7203                manifest_digest: &verification.manifest_digest,
7204                checkpoint_number: FIXTURE_BLOCK,
7205                checkpoint_hash: FIXTURE_BLOCK_HASH,
7206                checkpoint_parent_hash:
7207                    "0x0000000000000000000000000000000000000000000000000000000000000001",
7208                checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7209                checkpoint_base_fee_per_gas: Some(100_000_000),
7210                finalized_headers,
7211                next_canonical_nonce,
7212                observed_canonical_nonce,
7213                provider_ids: &provider_ids,
7214                operator_ids: &operator_ids,
7215                failure_domain_ids: &failure_domain_ids,
7216                decisions: &decisions,
7217                migration: None,
7218            })
7219            .await
7220    }
7221
7222    async fn initialize_test_verification_migration(
7223        client: &BlockchainExecutionClient,
7224        finalized_headers: &[ExecutionVerifiedHeader],
7225        next_canonical_nonce: u64,
7226        decisions: &[ExecutionVerificationDecision],
7227        migration: &ExecutionVerificationMigration,
7228    ) {
7229        let verification = client.config.verification.as_ref().unwrap();
7230        let provider_ids = vec![
7231            "authoritative".to_string(),
7232            "verifier-a".to_string(),
7233            "verifier-b".to_string(),
7234        ];
7235        let operator_ids = vec![
7236            "operator-a".to_string(),
7237            "operator-b".to_string(),
7238            "operator-c".to_string(),
7239        ];
7240        let failure_domain_ids = vec![
7241            "domain-a".to_string(),
7242            "domain-b".to_string(),
7243            "domain-c".to_string(),
7244        ];
7245        client
7246            .cache
7247            .database
7248            .as_ref()
7249            .unwrap()
7250            .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7251                chain_id: 42_161,
7252                wallet_address: WALLET,
7253                manifest_version: &verification.manifest_version,
7254                manifest_digest: &verification.manifest_digest,
7255                checkpoint_number: FIXTURE_BLOCK,
7256                checkpoint_hash: FIXTURE_BLOCK_HASH,
7257                checkpoint_parent_hash:
7258                    "0x0000000000000000000000000000000000000000000000000000000000000001",
7259                checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7260                checkpoint_base_fee_per_gas: Some(100_000_000),
7261                finalized_headers,
7262                next_canonical_nonce,
7263                observed_canonical_nonce: next_canonical_nonce,
7264                provider_ids: &provider_ids,
7265                operator_ids: &operator_ids,
7266                failure_domain_ids: &failure_domain_ids,
7267                decisions,
7268                migration: Some(migration),
7269            })
7270            .await
7271            .unwrap();
7272    }
7273
7274    async fn swap_rpc_state() -> MockRpcState {
7275        let min_amount_out = expected_min_amount_out(50);
7276        swap_rpc_state_with_min_amount_out(min_amount_out).await
7277    }
7278
7279    async fn swap_rpc_state_with_min_amount_out(min_amount_out: U256) -> MockRpcState {
7280        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
7281        finalized_swap_rpc_state(tx_hash, min_amount_out)
7282    }
7283
7284    /// The swap state with a broadcast response whose hash differs from the signed hash.
7285    async fn swap_rpc_state_for_mismatch() -> MockRpcState {
7286        swap_rpc_state()
7287            .await
7288            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION)
7289    }
7290
7291    /// Extracts the transaction awaiting finality in the in-flight slot.
7292    fn awaiting_in_flight(client: &BlockchainExecutionClient) -> InFlightTransaction {
7293        let slot = *client.in_flight.lock();
7294        let Some(InFlightSlot::AwaitingFinality(in_flight)) = slot else {
7295            panic!("expected an awaiting-finality transaction, was {slot:?}");
7296        };
7297        in_flight
7298    }
7299
7300    fn recovering_in_flight(client: &BlockchainExecutionClient) -> RecoveryTransaction {
7301        let slot = *client.in_flight.lock();
7302        let Some(InFlightSlot::Recovering(recovery)) = slot else {
7303            panic!("expected a recovery transaction, was {slot:?}");
7304        };
7305        recovery
7306    }
7307
7308    async fn execution_intent_markers(
7309        admin_pool: &sqlx::PgPool,
7310        schema: &str,
7311    ) -> Vec<(String, String, bool, bool)> {
7312        sqlx::query_as(sqlx::AssertSqlSafe(format!(
7313            "SELECT purpose, status, terminal_emitted, active \
7314             FROM {schema}.execution_intent ORDER BY id"
7315        )))
7316        .fetch_all(admin_pool)
7317        .await
7318        .unwrap()
7319    }
7320
7321    #[allow(unsafe_code)] // env-var mutation in tests; unique names avoid cross-test races
7322    fn payload_test_keys(
7323        active: [u8; 32],
7324        retired: Vec<[u8; 32]>,
7325        deployment_id: &str,
7326    ) -> PayloadKeySet {
7327        static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7328
7329        let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7330        let active_env = format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_ACTIVE");
7331        let retired_envs = retired
7332            .iter()
7333            .enumerate()
7334            .map(|(index, _)| format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_RETIRED_{index}"))
7335            .collect::<Vec<_>>();
7336        // SAFETY: each invocation uses unique variable names and removes them before returning
7337        unsafe { std::env::set_var(&active_env, hex::encode(active)) };
7338        for (env, key) in retired_envs.iter().zip(&retired) {
7339            // SAFETY: the retired variable name is unique to this invocation
7340            unsafe { std::env::set_var(env, hex::encode(key)) };
7341        }
7342
7343        let keys = PayloadKeySet::load(Some(&active_env), &retired_envs, Some(deployment_id))
7344            .unwrap()
7345            .unwrap();
7346
7347        // SAFETY: no other test or thread uses these unique variable names
7348        unsafe { std::env::remove_var(active_env) };
7349        for env in retired_envs {
7350            // SAFETY: the retired variable name is unique to this invocation
7351            unsafe { std::env::remove_var(env) };
7352        }
7353        keys
7354    }
7355
7356    #[allow(unsafe_code)] // env-var mutation in tests; unique names avoid cross-test races
7357    fn set_test_payload_key(
7358        client: &mut BlockchainExecutionClient,
7359        key: [u8; 32],
7360        deployment_id: &str,
7361    ) -> PayloadKeySet {
7362        static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7363
7364        let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7365        let active_env = format!("BLOCKCHAIN_TEST_EXECUTION_KEY_{env_id}");
7366        // SAFETY: each invocation uses a unique variable name retained for reconnect tests
7367        unsafe { std::env::set_var(&active_env, hex::encode(key)) };
7368        client.config.payload_key_env = Some(active_env);
7369        client.config.payload_deployment_id = Some(deployment_id.to_string());
7370        client.load_payload_keys().unwrap().unwrap()
7371    }
7372
7373    async fn protect_test_storage(client: &mut BlockchainExecutionClient, deployment_id: &str) {
7374        let keys = set_test_payload_key(client, [0xa5; 32], deployment_id);
7375        client
7376            .cache
7377            .database
7378            .as_ref()
7379            .unwrap()
7380            .ensure_execution_payload_storage(&keys)
7381            .await
7382            .unwrap();
7383        client.payload_keys = Some(Arc::new(keys));
7384    }
7385
7386    async fn reserve_test_wrap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7387        reserve_test_wrap_intent_for_wallet(database, WALLET).await
7388    }
7389
7390    async fn reserve_test_wrap_intent_for_wallet(
7391        database: &BlockchainCacheDatabase,
7392        wallet: &str,
7393    ) -> ExecutionIntentRow {
7394        database
7395            .reserve_execution_intent(&ExecutionIntentInsert {
7396                chain_id: 42161,
7397                wallet_address: wallet.to_string(),
7398                purpose: "wrap".to_string(),
7399                client_order_id: None,
7400                trader_id: None,
7401                strategy_id: None,
7402                account_id: None,
7403                instrument_id: None,
7404                pool_address: None,
7405                transaction_to: WETH_ADDRESS.to_string(),
7406                transaction_input: "0xd0e30db0".to_string(),
7407                transaction_value: "1".to_string(),
7408                amount_in: None,
7409                created_block: FIXTURE_BLOCK,
7410            })
7411            .await
7412            .unwrap()
7413    }
7414
7415    async fn persist_test_wrap_broadcast(
7416        database: &BlockchainCacheDatabase,
7417        keys: Option<&PayloadKeySet>,
7418    ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7419        let intent = reserve_test_wrap_intent(database).await;
7420        database
7421            .assign_execution_intent_nonce(intent.id, 7)
7422            .await
7423            .unwrap();
7424        let intent = database.get_execution_intent(intent.id).await.unwrap();
7425        let transaction = build_eip1559_transaction(
7426            42161,
7427            7,
7428            78_000,
7429            130_000_000,
7430            10_000_000,
7431            WETH_ADDRESS,
7432            U256::from(1u64),
7433            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
7434        );
7435        let (tx_hash, raw_tx) = sign_eip1559_transaction(
7436            transaction,
7437            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7438        )
7439        .await
7440        .unwrap();
7441        persist_test_payload(database, keys, &intent, tx_hash, &raw_tx).await;
7442        database
7443            .record_execution_status(
7444                intent.id,
7445                &tx_hash.to_string(),
7446                TransactionStatus::Broadcast,
7447                None,
7448                None,
7449                None,
7450                None,
7451                None,
7452            )
7453            .await
7454            .unwrap();
7455        (intent, tx_hash, raw_tx)
7456    }
7457
7458    async fn reserve_test_swap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7459        let pool = test_pool();
7460        let order = test_market_sell_order(pool.instrument_id);
7461        let calldata = expected_swap_calldata(expected_min_amount_out(50));
7462
7463        database
7464            .reserve_execution_intent(&ExecutionIntentInsert {
7465                chain_id: 42161,
7466                wallet_address: WALLET.to_string(),
7467                purpose: "swap".to_string(),
7468                client_order_id: Some(order.client_order_id().to_string()),
7469                trader_id: Some(order.trader_id().to_string()),
7470                strategy_id: Some(order.strategy_id().to_string()),
7471                account_id: Some("BLOCKCHAIN-001".to_string()),
7472                instrument_id: Some(pool.instrument_id.to_string()),
7473                pool_address: Some(pool.address.to_string()),
7474                transaction_to: ROUTER_ADDRESS.to_string(),
7475                transaction_input: hex::encode_prefixed(&calldata),
7476                transaction_value: U256::ZERO.to_string(),
7477                amount_in: Some("1000000000000000".to_string()),
7478                created_block: FIXTURE_BLOCK,
7479            })
7480            .await
7481            .unwrap()
7482    }
7483
7484    async fn persist_invalid_test_swap(
7485        database: &BlockchainCacheDatabase,
7486        keys: Option<&PayloadKeySet>,
7487    ) -> (ExecutionIntentRow, B256) {
7488        let intent = reserve_test_swap_intent(database).await;
7489        database
7490            .assign_execution_intent_nonce(intent.id, 7)
7491            .await
7492            .unwrap();
7493        let intent = database.get_execution_intent(intent.id).await.unwrap();
7494        let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7495        let mut raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7496        raw_transaction.push(0xff);
7497        persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7498        database
7499            .record_execution_status(
7500                intent.id,
7501                &tx_hash.to_string(),
7502                TransactionStatus::Broadcast,
7503                None,
7504                None,
7505                None,
7506                None,
7507                None,
7508            )
7509            .await
7510            .unwrap();
7511
7512        (intent, tx_hash)
7513    }
7514
7515    async fn persist_test_swap_broadcast(
7516        database: &BlockchainCacheDatabase,
7517        keys: Option<&PayloadKeySet>,
7518    ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7519        let intent = reserve_test_swap_intent(database).await;
7520        database
7521            .assign_execution_intent_nonce(intent.id, 7)
7522            .await
7523            .unwrap();
7524        let intent = database.get_execution_intent(intent.id).await.unwrap();
7525        let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7526        let raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7527        persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7528        database
7529            .record_execution_status(
7530                intent.id,
7531                &tx_hash.to_string(),
7532                TransactionStatus::Broadcast,
7533                None,
7534                None,
7535                None,
7536                None,
7537                None,
7538            )
7539            .await
7540            .unwrap();
7541        (intent, tx_hash, raw_transaction)
7542    }
7543
7544    async fn persist_test_payload(
7545        database: &BlockchainCacheDatabase,
7546        keys: Option<&PayloadKeySet>,
7547        intent: &ExecutionIntentRow,
7548        tx_hash: B256,
7549        raw_transaction: &[u8],
7550    ) {
7551        if let Some(keys) = keys {
7552            database.reserve_execution_payload_seal(keys).await.unwrap();
7553            let transaction_hash = tx_hash.to_string();
7554            let context =
7555                payload_context_identity(intent, &transaction_hash, 42_161, keys.deployment_id())
7556                    .unwrap();
7557            let envelope = keys.seal(raw_transaction, &context).unwrap();
7558            database
7559                .add_execution_transaction_envelope(intent.id, 42_161, &transaction_hash, &envelope)
7560                .await
7561                .unwrap();
7562        } else {
7563            database
7564                .add_execution_transaction_hash(
7565                    intent.id,
7566                    42_161,
7567                    &tx_hash.to_string(),
7568                    raw_transaction,
7569                )
7570                .await
7571                .unwrap();
7572        }
7573    }
7574
7575    async fn later_reconnect(
7576        previous: BlockchainExecutionClient,
7577        http_rpc_url: String,
7578    ) -> anyhow::Error {
7579        let database = previous.cache.database.as_ref().unwrap().clone();
7580        let payload_keys = previous.payload_keys.clone();
7581        drop(previous);
7582        let mut next = test_client(http_rpc_url);
7583        next.cache.database = Some(database);
7584        next.payload_keys = payload_keys;
7585        next.signer = Some(Arc::new(
7586            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7587        ));
7588        next.reconcile_unresolved_execution().await.unwrap_err()
7589    }
7590
7591    fn start_with_events(
7592        client: &mut BlockchainExecutionClient,
7593    ) -> tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent> {
7594        let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
7595        replace_exec_event_sender(sender);
7596        client.start().unwrap();
7597        receiver
7598    }
7599
7600    async fn await_pending_tasks(client: &BlockchainExecutionClient) {
7601        tokio::time::timeout(TEST_TIMEOUT, async {
7602            while !client.pending_tasks.all_finished() {
7603                tokio::time::sleep(Duration::from_millis(1)).await;
7604            }
7605        })
7606        .await
7607        .unwrap();
7608    }
7609
7610    fn collect_order_events(
7611        receiver: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
7612    ) -> Vec<OrderEventAny> {
7613        let mut events = Vec::new();
7614
7615        while let Ok(event) = receiver.try_recv() {
7616            if let ExecutionEvent::Order(order_event) = event {
7617                events.push(order_event);
7618            }
7619        }
7620        events
7621    }
7622
7623    fn assert_swap_quarantined_without_terminal_event(events: &[OrderEventAny]) {
7624        assert_eq!(events.len(), 1, "was: {events:?}");
7625        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7626    }
7627
7628    fn assert_swap_submitted_and_filled(events: &[OrderEventAny]) {
7629        assert_eq!(events.len(), 2, "was: {events:?}");
7630        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7631        assert!(matches!(&events[1], OrderEventAny::Filled(_)));
7632    }
7633
7634    fn expected_swap_calldata(min_amount_out: U256) -> Vec<u8> {
7635        UniswapV3SwapRouter::exactInputSingleCall {
7636            params: UniswapV3SwapRouter::ExactInputSingleParams {
7637                tokenIn: WETH_ADDRESS,
7638                tokenOut: address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
7639                fee: U24::try_from(500u32).unwrap(),
7640                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7641                deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7642                amountIn: U256::from(1_000_000_000_000_000u64),
7643                amountOutMinimum: min_amount_out,
7644                sqrtPriceLimitX96: U160::ZERO,
7645            },
7646        }
7647        .abi_encode()
7648    }
7649
7650    /// Derives the expected minimum output with the same live profiler the plan used.
7651    fn expected_min_amount_out(slippage_bps: u32) -> U256 {
7652        expected_min_amount_out_for(&test_pool(), true, slippage_bps)
7653    }
7654
7655    fn expected_min_amount_out_for(pool: &Pool, zero_for_one: bool, slippage_bps: u32) -> U256 {
7656        let profiler = test_profiler(pool, FIXTURE_BLOCK);
7657        let quote = profiler
7658            .swap_exact_in(U256::from(1_000_000_000_000_000u64), zero_for_one, None)
7659            .unwrap();
7660        let quoted = exact_output_amount(&quote, zero_for_one).unwrap();
7661        derive_min_amount_out(quoted, slippage_bps).unwrap()
7662    }
7663
7664    fn expected_sell_quote_amount() -> U256 {
7665        let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7666        let quote = profiler
7667            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
7668            .unwrap();
7669        exact_output_amount(&quote, true).unwrap()
7670    }
7671
7672    fn quote_response(amount: U256) -> String {
7673        let result = (amount, U160::from(1u128 << 96), 0u32, U256::from(50_000u64)).abi_encode();
7674        serde_json::json!({
7675            "jsonrpc": "2.0",
7676            "id": 1,
7677            "result": hex::encode_prefixed(result),
7678        })
7679        .to_string()
7680    }
7681
7682    async fn expected_swap_tx(min_amount_out: U256) -> (B256, String) {
7683        // The orchestration's policy math is deterministic: nonce 7 from the fixture,
7684        // buffered gas 78000, buffered max fee 130000000, priority fee 10000000
7685        let expected_tx = build_eip1559_transaction(
7686            42161,
7687            7,
7688            78_000,
7689            130_000_000,
7690            10_000_000,
7691            ROUTER_ADDRESS,
7692            U256::ZERO,
7693            Bytes::from(expected_swap_calldata(min_amount_out)),
7694        );
7695        sign_eip1559_transaction(
7696            expected_tx,
7697            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7698        )
7699        .await
7700        .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7701        .unwrap()
7702    }
7703
7704    fn expected_buy_base_amount() -> U256 {
7705        U256::from(1_000_000_000_000_000u64)
7706    }
7707
7708    fn expected_buy_amount_in() -> U256 {
7709        let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7710        profiler
7711            .swap_exact_out(expected_buy_base_amount(), false, None)
7712            .unwrap()
7713            .get_input_amount()
7714    }
7715
7716    fn expected_buy_min_amount_out(slippage_bps: u32) -> U256 {
7717        derive_min_amount_out(expected_buy_base_amount(), slippage_bps).unwrap()
7718    }
7719
7720    fn expected_buy_swap_calldata(min_amount_out: U256, amount_in: U256) -> Vec<u8> {
7721        UniswapV3SwapRouter::exactInputSingleCall {
7722            params: UniswapV3SwapRouter::ExactInputSingleParams {
7723                tokenIn: USDC_ADDRESS,
7724                tokenOut: WETH_ADDRESS,
7725                fee: U24::try_from(500u32).unwrap(),
7726                recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7727                deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7728                amountIn: amount_in,
7729                amountOutMinimum: min_amount_out,
7730                sqrtPriceLimitX96: U160::ZERO,
7731            },
7732        }
7733        .abi_encode()
7734    }
7735
7736    async fn expected_buy_swap_tx(min_amount_out: U256, amount_in: U256) -> (B256, String) {
7737        let expected_tx = build_eip1559_transaction(
7738            42161,
7739            7,
7740            78_000,
7741            130_000_000,
7742            10_000_000,
7743            ROUTER_ADDRESS,
7744            U256::ZERO,
7745            Bytes::from(expected_buy_swap_calldata(min_amount_out, amount_in)),
7746        );
7747        sign_eip1559_transaction(
7748            expected_tx,
7749            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7750        )
7751        .await
7752        .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7753        .unwrap()
7754    }
7755
7756    fn finalized_buy_swap_receipt(tx_hash: B256, amount_in: U256) -> String {
7757        finalized_buy_swap_receipt_with_base_out(tx_hash, amount_in, expected_buy_base_amount())
7758    }
7759
7760    fn finalized_buy_swap_receipt_with_base_out(
7761        tx_hash: B256,
7762        amount_in: U256,
7763        base_out: U256,
7764    ) -> String {
7765        let data = (
7766            -I256::try_from(u128::try_from(base_out).unwrap()).unwrap(),
7767            I256::try_from(u128::try_from(amount_in).unwrap()).unwrap(),
7768            U160::from(1_u128 << 96),
7769            TEST_LIQUIDITY,
7770            I24::try_from(0).unwrap(),
7771        )
7772            .abi_encode();
7773        serde_json::json!({
7774            "jsonrpc": "2.0",
7775            "id": 1,
7776            "result": {
7777                "transactionHash": tx_hash.to_string(),
7778                "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7779                "blockNumber": "0x1cf0d41",
7780                "transactionIndex": "0x2",
7781                "gasUsed": "0xc3c0",
7782                "effectiveGasPrice": "0x5f5e100",
7783                "status": "0x1",
7784                "logs": [{
7785                    "removed": false,
7786                    "logIndex": "0x6",
7787                    "transactionIndex": "0x2",
7788                    "transactionHash": tx_hash.to_string(),
7789                    "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7790                    "blockNumber": "0x1cf0d41",
7791                    "address": test_pool().address.to_string(),
7792                    "data": hex::encode_prefixed(data),
7793                    "topics": [
7794                        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7795                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7796                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7797                    ]
7798                }]
7799            }
7800        })
7801        .to_string()
7802    }
7803
7804    fn finalized_buy_swap_block(tx_hash: B256, min_amount_out: U256, amount_in: U256) -> String {
7805        serde_json::json!({
7806            "jsonrpc": "2.0",
7807            "id": 1,
7808            "result": {
7809                "number": "0x1cf0d41",
7810                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7811                "parentHash": FIXTURE_BLOCK_HASH,
7812                "timestamp": "0x69044a21",
7813                "baseFeePerGas": "0x5f5e100",
7814                "transactions": [{
7815                    "hash": tx_hash.to_string(),
7816                    "from": WALLET,
7817                    "nonce": "0x7",
7818                    "chainId": "0xa4b1",
7819                    "type": "0x2",
7820                    "to": ROUTER,
7821                    "input": hex::encode_prefixed(expected_buy_swap_calldata(min_amount_out, amount_in)),
7822                    "value": "0x0",
7823                    "gas": "0x130b0",
7824                    "maxFeePerGas": "0x7bfa480",
7825                    "maxPriorityFeePerGas": "0x989680"
7826                }]
7827            }
7828        })
7829        .to_string()
7830    }
7831
7832    fn finalized_buy_swap_rpc_state(
7833        tx_hash: B256,
7834        min_amount_out: U256,
7835        amount_in: U256,
7836    ) -> MockRpcState {
7837        let receipt = finalized_buy_swap_receipt(tx_hash, amount_in);
7838        let block = finalized_buy_swap_block(tx_hash, min_amount_out, amount_in);
7839        with_finalized_identity(
7840            signing_rpc_state()
7841                .with_response("eth_getTransactionReceipt", &receipt)
7842                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7843                .with_send_raw_transaction_echo()
7844                .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7845                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7846            &block,
7847            &receipt,
7848        )
7849    }
7850
7851    fn finalized_swap_receipt(tx_hash: B256) -> String {
7852        let data = (
7853            I256::try_from(1_000_000_000_000_000_i128).unwrap(),
7854            I256::try_from(-1_000_000_i128).unwrap(),
7855            U160::from(1_u128 << 96),
7856            TEST_LIQUIDITY,
7857            I24::try_from(0).unwrap(),
7858        )
7859            .abi_encode();
7860        serde_json::json!({
7861            "jsonrpc": "2.0",
7862            "id": 1,
7863            "result": {
7864                "transactionHash": tx_hash.to_string(),
7865                "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7866                "blockNumber": "0x1cf0d41",
7867                "transactionIndex": "0x2",
7868                "gasUsed": "0xc3c0",
7869                "effectiveGasPrice": "0x5f5e100",
7870                "status": "0x1",
7871                "logs": [{
7872                    "removed": false,
7873                    "logIndex": "0x6",
7874                    "transactionIndex": "0x2",
7875                    "transactionHash": tx_hash.to_string(),
7876                    "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7877                    "blockNumber": "0x1cf0d41",
7878                    "address": test_pool().address.to_string(),
7879                    "data": hex::encode_prefixed(data),
7880                    "topics": [
7881                        "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7882                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7883                        "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7884                    ]
7885                }]
7886            }
7887        })
7888        .to_string()
7889    }
7890
7891    fn finalized_swap_receipt_with_unrelated_swap(tx_hash: B256) -> String {
7892        let mut receipt: serde_json::Value =
7893            serde_json::from_str(&finalized_swap_receipt(tx_hash)).unwrap();
7894        let logs = receipt["result"]["logs"].as_array_mut().unwrap();
7895        let mut unrelated = logs[0].clone();
7896        unrelated["address"] = serde_json::json!(ROUTER);
7897        unrelated["logIndex"] = serde_json::json!("0x7");
7898        logs.push(unrelated);
7899        receipt.to_string()
7900    }
7901
7902    fn finalized_swap_block(tx_hash: B256, min_amount_out: U256) -> String {
7903        serde_json::json!({
7904            "jsonrpc": "2.0",
7905            "id": 1,
7906            "result": {
7907                "number": "0x1cf0d41",
7908                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7909                "parentHash": FIXTURE_BLOCK_HASH,
7910                "timestamp": "0x69044a21",
7911                "baseFeePerGas": "0x5f5e100",
7912                "transactions": [{
7913                    "hash": tx_hash.to_string(),
7914                    "from": WALLET,
7915                    "nonce": "0x7",
7916                    "chainId": "0xa4b1",
7917                    "type": "0x2",
7918                    "to": ROUTER,
7919                    "input": hex::encode_prefixed(expected_swap_calldata(min_amount_out)),
7920                    "value": "0x0",
7921                    "gas": "0x130b0",
7922                    "maxFeePerGas": "0x7bfa480",
7923                    "maxPriorityFeePerGas": "0x989680"
7924                }]
7925            }
7926        })
7927        .to_string()
7928    }
7929
7930    fn finalized_swap_rpc_state(tx_hash: B256, min_amount_out: U256) -> MockRpcState {
7931        let receipt = finalized_swap_receipt(tx_hash);
7932        let block = finalized_swap_block(tx_hash, min_amount_out);
7933        with_finalized_identity(
7934            signing_rpc_state()
7935                .with_response("eth_getTransactionReceipt", &receipt)
7936                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7937                .with_send_raw_transaction_echo()
7938                .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7939                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7940            &block,
7941            &receipt,
7942        )
7943    }
7944
7945    fn with_finalized_identity(
7946        state: MockRpcState,
7947        block_response: &str,
7948        receipt_response: &str,
7949    ) -> MockRpcState {
7950        let block: serde_json::Value = serde_json::from_str(block_response).unwrap();
7951        let receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
7952        let transaction = block["result"]["transactions"][0].clone();
7953        let transaction_response = serde_json::json!({
7954            "jsonrpc": "2.0",
7955            "id": 1,
7956            "result": transaction,
7957        })
7958        .to_string();
7959        let success = receipt["result"]["status"] == "0x1";
7960        let mut trace = serde_json::json!({
7961            "type": "CALL",
7962            "from": transaction["from"],
7963            "to": transaction["to"],
7964            "value": transaction["value"],
7965            "gas": transaction["gas"],
7966            "gasUsed": receipt["result"]["gasUsed"],
7967            "input": transaction["input"],
7968            "output": "0x",
7969            "calls": [],
7970        });
7971
7972        if !success {
7973            trace["error"] = serde_json::json!("execution reverted");
7974        }
7975        let trace_response = serde_json::json!({
7976            "jsonrpc": "2.0",
7977            "id": 1,
7978            "result": trace,
7979        })
7980        .to_string();
7981        state
7982            .with_response("eth_getTransactionByHash", &transaction_response)
7983            .with_response("debug_traceTransaction", &trace_response)
7984    }
7985
7986    fn receipt_with_transaction_hash(receipt_response: &str, tx_hash: B256) -> String {
7987        let mut receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
7988        receipt["result"]["transactionHash"] = serde_json::json!(tx_hash.to_string());
7989        receipt.to_string()
7990    }
7991
7992    fn replacement_head_block(tx_hash: B256) -> String {
7993        serde_json::json!({
7994            "jsonrpc": "2.0",
7995            "id": 1,
7996            "result": {
7997                "number": "0x1cf0d40",
7998                "hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
7999                "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001",
8000                "timestamp": "0x69044a20",
8001                "baseFeePerGas": "0x5f5e100",
8002                "transactions": [{
8003                    "hash": "0x3333333333333333333333333333333333333333333333333333333333333333",
8004                    "from": "0x0000000000000000000000000000000000000001",
8005                    "nonce": "0x1",
8006                    "type": "0x0",
8007                    "to": WETH,
8008                    "input": "0x",
8009                    "value": "0x0",
8010                    "gas": "0x5208"
8011                }, {
8012                    "hash": tx_hash.to_string(),
8013                    "from": WALLET,
8014                    "nonce": "0x7",
8015                    "chainId": "0xa4b1",
8016                    "type": "0x2",
8017                    "to": WETH,
8018                    "input": "0xd0e30db0",
8019                    "value": "0x38d7ea4c68000",
8020                    "gas": "0x130b0",
8021                    "maxFeePerGas": "0x7bfa480",
8022                    "maxPriorityFeePerGas": "0x989680"
8023                }]
8024            }
8025        })
8026        .to_string()
8027    }
8028
8029    /// The canonical block at the receipt height containing the given wrap transaction with
8030    /// the exact persisted call fields.
8031    fn finalized_wrap_block(tx_hash: B256) -> String {
8032        serde_json::json!({
8033            "jsonrpc": "2.0",
8034            "id": 1,
8035            "result": {
8036                "number": "0x1cf0d41",
8037                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8038                "parentHash": FIXTURE_BLOCK_HASH,
8039                "timestamp": "0x69044a21",
8040                "baseFeePerGas": "0x5f5e100",
8041                "transactions": [{
8042                    "hash": tx_hash.to_string(),
8043                    "from": WALLET,
8044                    "nonce": "0x7",
8045                    "chainId": "0xa4b1",
8046                    "type": "0x2",
8047                    "to": WETH,
8048                    "input": "0xd0e30db0",
8049                    "value": "0x38d7ea4c68000",
8050                    "gas": "0x130b0",
8051                    "maxFeePerGas": "0x7bfa480",
8052                    "maxPriorityFeePerGas": "0x989680"
8053                }]
8054            }
8055        })
8056        .to_string()
8057    }
8058
8059    /// The canonical block at the receipt height containing the given approve transaction
8060    /// with the exact persisted call fields.
8061    fn finalized_approve_block(tx_hash: B256, amount: U256) -> String {
8062        let calldata = ERC20::approveCall {
8063            spender: ROUTER_ADDRESS,
8064            amount,
8065        }
8066        .abi_encode();
8067        serde_json::json!({
8068            "jsonrpc": "2.0",
8069            "id": 1,
8070            "result": {
8071                "number": "0x1cf0d41",
8072                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8073                "parentHash": FIXTURE_BLOCK_HASH,
8074                "timestamp": "0x69044a21",
8075                "baseFeePerGas": "0x5f5e100",
8076                "transactions": [{
8077                    "hash": tx_hash.to_string(),
8078                    "from": WALLET,
8079                    "nonce": "0x7",
8080                    "chainId": "0xa4b1",
8081                    "type": "0x2",
8082                    "to": WETH,
8083                    "input": hex::encode_prefixed(calldata),
8084                    "value": "0x0",
8085                    "gas": "0x130b0",
8086                    "maxFeePerGas": "0x7bfa480",
8087                    "maxPriorityFeePerGas": "0x989680"
8088                }]
8089            }
8090        })
8091        .to_string()
8092    }
8093
8094    fn fixture_sell_plan() -> SwapPlan {
8095        let pool = test_pool();
8096        let order = test_market_sell_order(pool.instrument_id);
8097        let quote_token = pool.get_quote_token();
8098        SwapPlan {
8099            order,
8100            quote_currency: Currency::new_checked(
8101                &quote_token.symbol,
8102                quote_token.decimals,
8103                0,
8104                &quote_token.name,
8105                CurrencyType::Crypto,
8106            )
8107            .unwrap(),
8108            instrument_id: pool.instrument_id,
8109            pool_address: pool.address,
8110            router: ROUTER_ADDRESS,
8111            factory: UNISWAP_V3.dex.factory,
8112            weth: WETH_ADDRESS,
8113            token_in: WETH_ADDRESS,
8114            token_out: USDC_ADDRESS,
8115            fee: U24::try_from(500u32).unwrap(),
8116            amount_in: U256::from(1_000_000_000_000_000u64),
8117            min_amount_out: expected_min_amount_out(50),
8118            slippage_bps: 50,
8119            quote_spend_ceiling: None,
8120            profiler_position: Some(profiler_event_position()),
8121            pool,
8122        }
8123    }
8124
8125    fn fixture_block_response(number: u64, hash: B256) -> String {
8126        let parent_hash = if number == FIXTURE_BLOCK {
8127            B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001")
8128                .unwrap()
8129        } else {
8130            B256::from_str(FIXTURE_BLOCK_HASH).unwrap()
8131        };
8132        serde_json::json!({
8133            "jsonrpc": "2.0",
8134            "id": 1,
8135            "result": {
8136                "number": format!("0x{number:x}"),
8137                "hash": hash.to_string(),
8138                "parentHash": parent_hash.to_string(),
8139                "timestamp": format!("0x{:x}", FIXTURE_BLOCK_TIMESTAMP + number - FIXTURE_BLOCK),
8140                "baseFeePerGas": "0x5f5e100",
8141                "transactions": []
8142            }
8143        })
8144        .to_string()
8145    }
8146
8147    fn profiler_event_receipt(pool_address: Address) -> String {
8148        let transaction_hash = B256::from([0x33; 32]);
8149        serde_json::json!({
8150            "jsonrpc": "2.0",
8151            "id": 1,
8152            "result": {
8153                "transactionHash": transaction_hash.to_string(),
8154                "blockHash": FIXTURE_BLOCK_HASH,
8155                "blockNumber": "0x1cf0d40",
8156                "transactionIndex": "0x2",
8157                "gasUsed": "0xc3c0",
8158                "effectiveGasPrice": "0x5f5e100",
8159                "status": "0x1",
8160                "logs": [{
8161                    "removed": false,
8162                    "logIndex": "0x6",
8163                    "transactionIndex": "0x2",
8164                    "transactionHash": transaction_hash.to_string(),
8165                    "blockHash": FIXTURE_BLOCK_HASH,
8166                    "blockNumber": "0x1cf0d40",
8167                    "address": pool_address.to_string(),
8168                    "data": "0x",
8169                    "topics": [test_pool().dex.swap_created_event.as_ref()]
8170                }]
8171            }
8172        })
8173        .to_string()
8174    }
8175
8176    fn profiler_event_position() -> BlockPosition {
8177        BlockPosition::new(FIXTURE_BLOCK, B256::from([0x33; 32]).to_string(), 2, 6)
8178            .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()))
8179    }
8180
8181    #[rstest]
8182    fn quantity_to_raw_amount_scales_by_token_decimals() {
8183        assert_eq!(
8184            quantity_to_raw_amount(Quantity::from("0.001"), 18).unwrap(),
8185            U256::from(1_000_000_000_000_000u64)
8186        );
8187        assert_eq!(
8188            quantity_to_raw_amount(Quantity::from("1.5"), 18).unwrap(),
8189            U256::from(1_500_000_000_000_000_000u128)
8190        );
8191        assert_eq!(
8192            quantity_to_raw_amount(Quantity::from("12.5"), 6).unwrap(),
8193            U256::from(12_500_000u64)
8194        );
8195    }
8196
8197    #[rstest]
8198    fn quantity_to_raw_amount_uses_defi_quantity_precision() {
8199        let amount = U256::from(10_000_000_000_000_000u64);
8200        let quantity = Quantity::from_u256(amount, 18).unwrap();
8201
8202        assert_eq!(quantity_to_raw_amount(quantity, 18).unwrap(), amount);
8203    }
8204
8205    #[rstest]
8206    fn quantity_to_raw_amount_rejects_zero() {
8207        let error = quantity_to_raw_amount(Quantity::from("0.0"), 18).unwrap_err();
8208
8209        assert_eq!(error.to_string(), "Order quantity must be positive");
8210    }
8211
8212    #[rstest]
8213    fn quantity_to_raw_amount_rejects_inexact_token_units() {
8214        let error = quantity_to_raw_amount(Quantity::from("0.0000001"), 6).unwrap_err();
8215
8216        assert!(
8217            error
8218                .to_string()
8219                .contains("is not exactly representable in 6 base token decimals"),
8220            "was: {error}"
8221        );
8222    }
8223
8224    #[rstest]
8225    fn raw_amount_to_quantity_inverts_base_quantity() {
8226        let quantity = Quantity::from("0.001");
8227        let amount = quantity_to_raw_amount(quantity, 18).unwrap();
8228
8229        assert_eq!(raw_amount_to_quantity(amount, 18).unwrap(), quantity);
8230    }
8231
8232    #[rstest]
8233    fn raw_amount_to_quantity_rejects_positive_amount_truncated_to_zero() {
8234        let error = raw_amount_to_quantity(U256::from(99u64), 18).unwrap_err();
8235
8236        assert!(
8237            error
8238                .to_string()
8239                .contains("is below representable quantity precision"),
8240            "was: {error}"
8241        );
8242    }
8243
8244    #[rstest]
8245    fn fill_price_from_quote_recovers_spent_quote_after_quantity_truncation() {
8246        let last_qty = raw_amount_to_quantity(U256::from(1_000_000_403_079_044u64), 18).unwrap();
8247        let quote_amount = U256::from(1_891_348u64);
8248        let quote = Currency::new_checked("USDC", 6, 0, "USD Coin", CurrencyType::Crypto).unwrap();
8249        let last_px = fill_price_from_quote(last_qty, quote_amount, quote).unwrap();
8250
8251        assert_eq!(
8252            Money::from_decimal(last_qty.as_decimal() * last_px.as_decimal(), quote).unwrap(),
8253            Money::from_u256(quote_amount, quote).unwrap()
8254        );
8255    }
8256
8257    #[rstest]
8258    fn swap_token_pair_is_directional() {
8259        assert_eq!(
8260            swap_token_pair(OrderSide::Sell, WETH_ADDRESS, USDC_ADDRESS).unwrap(),
8261            (WETH_ADDRESS, USDC_ADDRESS)
8262        );
8263        assert_eq!(
8264            swap_token_pair(OrderSide::Buy, WETH_ADDRESS, USDC_ADDRESS).unwrap(),
8265            (USDC_ADDRESS, WETH_ADDRESS)
8266        );
8267    }
8268
8269    #[rstest]
8270    fn restore_swap_plan_buy_uses_quote_input() {
8271        let (client, cache) =
8272            swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8273        let order = test_market_buy_order(test_pool().instrument_id);
8274        cache
8275            .borrow_mut()
8276            .add_order(order.clone(), None, None, true)
8277            .unwrap();
8278        let amount_in = U256::from(2_345_678u64);
8279        let intent = ExecutionIntentRow {
8280            id: 7,
8281            schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
8282            chain_id: 42161,
8283            wallet_address: WALLET.to_string(),
8284            nonce: Some(7),
8285            purpose: "swap".to_string(),
8286            status: "finalized".to_string(),
8287            client_order_id: Some(order.client_order_id().to_string()),
8288            trader_id: Some(order.trader_id().to_string()),
8289            strategy_id: Some(order.strategy_id().to_string()),
8290            account_id: Some("BLOCKCHAIN-001".to_string()),
8291            instrument_id: Some(order.instrument_id().to_string()),
8292            pool_address: Some(test_pool().address.to_string()),
8293            transaction_to: ROUTER.to_string(),
8294            transaction_input: "0x".to_string(),
8295            transaction_value: "0".to_string(),
8296            amount_in: Some(amount_in.to_string()),
8297            created_block: FIXTURE_BLOCK,
8298            acknowledgement_emitted: true,
8299            fill_emitted: false,
8300            terminal_emitted: false,
8301            active: true,
8302        };
8303
8304        let plan = client.restore_swap_plan(&intent).unwrap();
8305
8306        assert_eq!(plan.token_in, USDC_ADDRESS);
8307        assert_eq!(plan.token_out, WETH_ADDRESS);
8308        assert_eq!(plan.amount_in, amount_in);
8309    }
8310
8311    #[rstest]
8312    #[case(1_000_000, 50, 995_000)]
8313    #[case(1_000_000, 0, 1_000_000)]
8314    #[case(1_000_000, 200, 980_000)]
8315    #[case(10_000, 9_999, 1)]
8316    fn derive_min_amount_out_applies_slippage(
8317        #[case] quoted: u64,
8318        #[case] slippage_bps: u32,
8319        #[case] expected: u64,
8320    ) {
8321        assert_eq!(
8322            derive_min_amount_out(U256::from(quoted), slippage_bps).unwrap(),
8323            U256::from(expected)
8324        );
8325    }
8326
8327    #[rstest]
8328    fn derive_min_amount_out_rejects_zero_result() {
8329        let error = derive_min_amount_out(U256::from(9_999u64), 9_999).unwrap_err();
8330
8331        assert!(
8332            error.to_string().contains("Derived minimum output is zero"),
8333            "was: {error}"
8334        );
8335    }
8336
8337    #[rstest]
8338    fn derive_min_amount_out_rejects_full_slippage() {
8339        let error = derive_min_amount_out(U256::from(1_000_000u64), 10_000).unwrap_err();
8340
8341        assert!(
8342            error.to_string().contains("must be below 10000"),
8343            "was: {error}"
8344        );
8345    }
8346
8347    #[rstest]
8348    fn replacement_scan_range_is_bounded_and_checked() {
8349        assert_eq!(
8350            replacement_scan_range(10, 10).unwrap(),
8351            RangeInclusive::new(10, 10)
8352        );
8353        assert_eq!(
8354            replacement_scan_range(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS).unwrap(),
8355            RangeInclusive::new(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS - 1)
8356        );
8357        assert!(
8358            replacement_scan_range(11, 10)
8359                .unwrap_err()
8360                .to_string()
8361                .contains("is behind execution creation block")
8362        );
8363        assert_eq!(
8364            replacement_scan_range(u64::MAX - 1, u64::MAX).unwrap(),
8365            RangeInclusive::new(u64::MAX - 1, u64::MAX)
8366        );
8367    }
8368
8369    #[rstest]
8370    fn terminal_execution_event_ids_are_stable_and_kind_specific() {
8371        let transaction_hash = B256::from([0x42; 32]);
8372
8373        let fill = execution_event_id(transaction_hash, b"fill");
8374        let fill_retry = execution_event_id(transaction_hash, b"fill");
8375        let reverted = execution_event_id(transaction_hash, b"reverted");
8376
8377        assert_eq!(fill, fill_retry);
8378        assert_ne!(fill, reverted);
8379    }
8380
8381    #[rstest]
8382    fn prepared_transaction_debug_redacts_raw_transaction() {
8383        let prepared = PreparedTransaction {
8384            intent_id: 1,
8385            created_block: 2,
8386            nonce: 3,
8387            tx_hash: B256::ZERO,
8388            raw_tx: vec![0xde, 0xad, 0xbe, 0xef],
8389            payload_lease: None,
8390        };
8391
8392        let debug = format!("{prepared:?}");
8393
8394        assert!(debug.contains("raw_tx: \"<redacted>\""));
8395        assert!(!debug.contains("[222, 173, 190, 239]"));
8396    }
8397
8398    #[rstest]
8399    fn call_trace_rejects_unreviewed_internal_edge() {
8400        let signed = crate::execution::transaction::DecodedSignedTransaction {
8401            hash: B256::from([1; 32]),
8402            signer: Address::from_str(WALLET).unwrap(),
8403            chain_id: 42_161,
8404            nonce: 7,
8405            to: ROUTER_ADDRESS,
8406            value: U256::ZERO,
8407            input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8408            gas_limit: 78_000,
8409            max_fee_per_gas: 130_000_000,
8410            max_priority_fee_per_gas: 10_000_000,
8411        };
8412        let input_digest = keccak256(&signed.input);
8413        let trace = VerifiedCallTrace {
8414            call_type: RpcCallType::Call,
8415            from: signed.signer,
8416            to: Some(signed.to),
8417            value: signed.value,
8418            input_selector: signed
8419                .input
8420                .get(..4)
8421                .map(|selector| selector.try_into().unwrap()),
8422            input_digest,
8423            success: true,
8424            calls: vec![VerifiedCallTrace {
8425                call_type: RpcCallType::Call,
8426                from: ROUTER_ADDRESS,
8427                to: Some(WETH_ADDRESS),
8428                value: U256::ZERO,
8429                input_selector: None,
8430                input_digest: B256::ZERO,
8431                success: true,
8432                calls: Vec::new(),
8433            }],
8434        };
8435        let manifest = &test_config("http://127.0.0.1:1".to_string())
8436            .verification
8437            .unwrap()
8438            .deployment_manifest;
8439
8440        let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8441
8442        assert!(
8443            error.to_string().contains("unreviewed call edge"),
8444            "was: {error}"
8445        );
8446    }
8447
8448    #[rstest]
8449    fn call_trace_rejects_unlisted_precompile_target() {
8450        let manifest = test_config("http://127.0.0.1:1".to_string())
8451            .verification
8452            .unwrap()
8453            .deployment_manifest;
8454        let calls = [VerifiedCallTrace {
8455            call_type: RpcCallType::Call,
8456            from: ROUTER_ADDRESS,
8457            to: Some(address!("0000000000000000000000000000000000000007")),
8458            value: U256::ZERO,
8459            input_selector: None,
8460            input_digest: B256::ZERO,
8461            success: true,
8462            calls: Vec::new(),
8463        }];
8464
8465        let error =
8466            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8467
8468        assert_eq!(
8469            error.to_string(),
8470            format!(
8471                "Verified call trace contains an unreviewed call edge {ROUTER_ADDRESS} -> {} for swap_sell",
8472                address!("0000000000000000000000000000000000000007")
8473            )
8474        );
8475    }
8476
8477    #[rstest]
8478    fn call_trace_requires_exact_call_type() {
8479        let manifest = test_config("http://127.0.0.1:1".to_string())
8480            .verification
8481            .unwrap()
8482            .deployment_manifest;
8483        let calls = [VerifiedCallTrace {
8484            call_type: RpcCallType::Staticcall,
8485            from: ROUTER_ADDRESS,
8486            to: Some(test_pool().address),
8487            value: U256::ZERO,
8488            input_selector: None,
8489            input_digest: B256::ZERO,
8490            success: true,
8491            calls: Vec::new(),
8492        }];
8493
8494        let error =
8495            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8496
8497        assert!(
8498            error.to_string().contains("unreviewed staticcall edge"),
8499            "was: {error}"
8500        );
8501    }
8502
8503    #[rstest]
8504    fn call_trace_requires_exact_caller() {
8505        let manifest = test_config("http://127.0.0.1:1".to_string())
8506            .verification
8507            .unwrap()
8508            .deployment_manifest;
8509        let calls = [VerifiedCallTrace {
8510            call_type: RpcCallType::Call,
8511            from: WETH_ADDRESS,
8512            to: Some(test_pool().address),
8513            value: U256::ZERO,
8514            input_selector: None,
8515            input_digest: B256::ZERO,
8516            success: true,
8517            calls: Vec::new(),
8518        }];
8519
8520        let error =
8521            validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8522
8523        assert_eq!(
8524            error.to_string(),
8525            "Verified call trace child has an invalid caller context"
8526        );
8527    }
8528
8529    #[rstest]
8530    fn call_trace_rejects_contract_creation() {
8531        let signed = crate::execution::transaction::DecodedSignedTransaction {
8532            hash: B256::from([1; 32]),
8533            signer: Address::from_str(WALLET).unwrap(),
8534            chain_id: 42_161,
8535            nonce: 7,
8536            to: ROUTER_ADDRESS,
8537            value: U256::ZERO,
8538            input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8539            gas_limit: 78_000,
8540            max_fee_per_gas: 130_000_000,
8541            max_priority_fee_per_gas: 10_000_000,
8542        };
8543        let trace = VerifiedCallTrace {
8544            call_type: RpcCallType::Call,
8545            from: signed.signer,
8546            to: Some(signed.to),
8547            value: signed.value,
8548            input_selector: signed
8549                .input
8550                .get(..4)
8551                .map(|selector| selector.try_into().unwrap()),
8552            input_digest: keccak256(&signed.input),
8553            success: true,
8554            calls: vec![VerifiedCallTrace {
8555                call_type: RpcCallType::Create,
8556                from: ROUTER_ADDRESS,
8557                to: Some(address!("0000000000000000000000000000000000000007")),
8558                value: U256::ZERO,
8559                input_selector: None,
8560                input_digest: B256::ZERO,
8561                success: true,
8562                calls: Vec::new(),
8563            }],
8564        };
8565        let manifest = &test_config("http://127.0.0.1:1".to_string())
8566            .verification
8567            .unwrap()
8568            .deployment_manifest;
8569
8570        let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8571
8572        assert!(
8573            error
8574                .to_string()
8575                .contains("forbidden state-changing operation"),
8576            "was: {error}"
8577        );
8578    }
8579
8580    #[tokio::test]
8581    async fn live_arbitrum_numbered_swap_reads_are_available() {
8582        if std::env::var(LIVE_READ_SMOKE_ENV).as_deref() != Ok("1") {
8583            eprintln!("{LIVE_READ_SMOKE_ENV} is not 1; skipping live read smoke");
8584            return;
8585        }
8586
8587        let rpc_url = std::env::var("ARBITRUM_RPC_HTTP_URL")
8588            .unwrap_or_else(|_| LIVE_READ_SMOKE_RPC.to_string());
8589        let rpc = Arc::new(BlockchainHttpRpcClient::new(rpc_url, None, None));
8590        let anchor = rpc.latest_block().await.unwrap();
8591        let pool = test_pool();
8592        let factory = UNISWAP_V3.dex.factory;
8593        let wallet = Address::from_str(WALLET).unwrap();
8594        let mut balance_call =
8595            nautilus_core::hex::decode(BALANCE_OF_SELECTOR.trim_start_matches("0x")).unwrap();
8596        balance_call.extend_from_slice(&[0; 12]);
8597        balance_call.extend_from_slice(wallet.as_slice());
8598
8599        let code = rpc
8600            .get_code_at(&ROUTER_ADDRESS, anchor.number)
8601            .await
8602            .unwrap();
8603        let router_factory = rpc
8604            .call_at(
8605                None,
8606                &ROUTER_ADDRESS,
8607                U256::ZERO,
8608                &UniswapV3RouterState::factoryCall {}.abi_encode(),
8609                anchor.number,
8610            )
8611            .await
8612            .unwrap();
8613        let router_factory =
8614            UniswapV3RouterState::factoryCall::abi_decode_returns(&router_factory).unwrap();
8615        let router_weth = rpc
8616            .call_at(
8617                None,
8618                &ROUTER_ADDRESS,
8619                U256::ZERO,
8620                &UniswapV3RouterState::WETH9Call {}.abi_encode(),
8621                anchor.number,
8622            )
8623            .await
8624            .unwrap();
8625        let router_weth =
8626            UniswapV3RouterState::WETH9Call::abi_decode_returns(&router_weth).unwrap();
8627        let registered_pool_call = UniswapV3Factory::getPoolCall {
8628            tokenA: WETH_ADDRESS,
8629            tokenB: USDC_ADDRESS,
8630            fee: U24::try_from(500u32).unwrap(),
8631        }
8632        .abi_encode();
8633        let registered_pool = rpc
8634            .call_at(
8635                None,
8636                &factory,
8637                U256::ZERO,
8638                &registered_pool_call,
8639                anchor.number,
8640            )
8641            .await
8642            .unwrap();
8643        let registered_pool =
8644            UniswapV3Factory::getPoolCall::abi_decode_returns(&registered_pool).unwrap();
8645        let weth_decimals = rpc
8646            .call_at(
8647                None,
8648                &WETH_ADDRESS,
8649                U256::ZERO,
8650                &ERC20::decimalsCall {}.abi_encode(),
8651                anchor.number,
8652            )
8653            .await
8654            .unwrap();
8655        let weth_decimals = ERC20::decimalsCall::abi_decode_returns(&weth_decimals).unwrap();
8656        let usdc_decimals = rpc
8657            .call_at(
8658                None,
8659                &USDC_ADDRESS,
8660                U256::ZERO,
8661                &ERC20::decimalsCall {}.abi_encode(),
8662                anchor.number,
8663            )
8664            .await
8665            .unwrap();
8666        let usdc_decimals = ERC20::decimalsCall::abi_decode_returns(&usdc_decimals).unwrap();
8667        let allowance_call = ERC20::allowanceCall {
8668            owner: wallet,
8669            spender: ROUTER_ADDRESS,
8670        }
8671        .abi_encode();
8672        rpc.call_at(
8673            None,
8674            &WETH_ADDRESS,
8675            U256::ZERO,
8676            &allowance_call,
8677            anchor.number,
8678        )
8679        .await
8680        .unwrap();
8681        let balance_call = ERC20::balanceOfCall { account: wallet }.abi_encode();
8682        rpc.call_at(
8683            None,
8684            &WETH_ADDRESS,
8685            U256::ZERO,
8686            &balance_call,
8687            anchor.number,
8688        )
8689        .await
8690        .unwrap();
8691        let gas = rpc
8692            .estimate_gas_at(
8693                &wallet,
8694                &WETH_ADDRESS,
8695                U256::ZERO,
8696                &balance_call,
8697                anchor.number,
8698            )
8699            .await
8700            .unwrap();
8701        rpc.get_balance_with_timeout(
8702            &wallet,
8703            Some(anchor.number),
8704            Some(EXECUTION_RPC_TIMEOUT_SECS),
8705        )
8706        .await
8707        .unwrap();
8708        let canonical = rpc.block_by_number(anchor.number, false).await.unwrap();
8709
8710        assert!(!code.is_empty());
8711        assert_eq!(router_factory, factory);
8712        assert_eq!(router_weth, WETH_ADDRESS);
8713        assert_eq!(registered_pool, pool.address);
8714        assert_eq!(weth_decimals, 18);
8715        assert_eq!(usdc_decimals, 6);
8716        assert!(gas > 0);
8717        assert_eq!(canonical.hash, anchor.hash);
8718    }
8719
8720    #[tokio::test]
8721    async fn swap_quote_rejects_missing_ingestion_block_hash() {
8722        let (client, state) = client_with_mock_rpc(execution_rpc_state()).await;
8723        let plan = fixture_sell_plan();
8724        let position = BlockPosition::new(
8725            FIXTURE_BLOCK,
8726            FIXTURE_BLOCK_HASH.to_string(),
8727            BLOCK_SCOPED_SNAPSHOT_INDEX,
8728            BLOCK_SCOPED_SNAPSHOT_INDEX,
8729        );
8730
8731        let error = validate_swap_quote(
8732            &position,
8733            &plan,
8734            100,
8735            &client.verification,
8736            &client
8737                .config
8738                .verification
8739                .as_ref()
8740                .unwrap()
8741                .deployment_manifest,
8742        )
8743        .await
8744        .unwrap_err();
8745
8746        assert!(
8747            error.to_string().contains("no ingestion-time block hash"),
8748            "was: {error}"
8749        );
8750        assert!(state.recorded_requests().is_empty());
8751    }
8752
8753    #[tokio::test]
8754    async fn swap_quote_rejects_replaced_ingestion_block() {
8755        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
8756        let state = execution_rpc_state().with_parameter_response(
8757            "eth_getBlockByNumber",
8758            "0x1cf0d40",
8759            &changed,
8760        );
8761        let (client, _) = client_with_mock_rpc(state).await;
8762        let plan = fixture_sell_plan();
8763        let position = BlockPosition::new(
8764            FIXTURE_BLOCK,
8765            FIXTURE_BLOCK_HASH.to_string(),
8766            BLOCK_SCOPED_SNAPSHOT_INDEX,
8767            BLOCK_SCOPED_SNAPSHOT_INDEX,
8768        )
8769        .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()));
8770
8771        let error = validate_swap_quote(
8772            &position,
8773            &plan,
8774            100,
8775            &client.verification,
8776            &client
8777                .config
8778                .verification
8779                .as_ref()
8780                .unwrap()
8781                .deployment_manifest,
8782        )
8783        .await
8784        .unwrap_err();
8785
8786        assert!(error.to_string().contains("changed from"), "was: {error}");
8787    }
8788
8789    #[tokio::test]
8790    async fn swap_quote_accepts_exact_canonical_event_watermark() {
8791        let pool = test_pool();
8792        let receipt = profiler_event_receipt(pool.address);
8793        let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8794        let (client, _) = client_with_mock_rpc(state).await;
8795        let plan = fixture_sell_plan();
8796
8797        let anchors = validate_swap_quote(
8798            &profiler_event_position(),
8799            &plan,
8800            100,
8801            &client.verification,
8802            &client
8803                .config
8804                .verification
8805                .as_ref()
8806                .unwrap()
8807                .deployment_manifest,
8808        )
8809        .await
8810        .unwrap();
8811
8812        assert_eq!(anchors.watermark.number, profiler_event_position().number);
8813        assert_eq!(anchors.state.number, FIXTURE_BLOCK);
8814        assert_eq!(anchors.state.hash, B256::from([0x11; 32]));
8815        assert_eq!(anchors.state.timestamp, FIXTURE_BLOCK_TIMESTAMP);
8816    }
8817
8818    #[tokio::test]
8819    async fn swap_quote_rejects_mismatched_receipt_position() {
8820        let pool = test_pool();
8821        let mut receipt: serde_json::Value =
8822            serde_json::from_str(&profiler_event_receipt(pool.address)).unwrap();
8823        receipt["result"]["transactionIndex"] = serde_json::json!("0x3");
8824        let state =
8825            execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt.to_string());
8826        let (client, _) = client_with_mock_rpc(state).await;
8827        let plan = fixture_sell_plan();
8828
8829        let error = validate_swap_quote(
8830            &profiler_event_position(),
8831            &plan,
8832            100,
8833            &client.verification,
8834            &client
8835                .config
8836                .verification
8837                .as_ref()
8838                .unwrap()
8839                .deployment_manifest,
8840        )
8841        .await
8842        .unwrap_err();
8843
8844        assert_eq!(
8845            error.to_string(),
8846            "Profiler receipt position does not match its ingestion watermark"
8847        );
8848    }
8849
8850    #[tokio::test]
8851    async fn swap_quote_rejects_watermark_from_different_pool() {
8852        let receipt = profiler_event_receipt(ROUTER_ADDRESS);
8853        let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8854        let (client, _) = client_with_mock_rpc(state).await;
8855        let plan = fixture_sell_plan();
8856
8857        let error = validate_swap_quote(
8858            &profiler_event_position(),
8859            &plan,
8860            100,
8861            &client.verification,
8862            &client
8863                .config
8864                .verification
8865                .as_ref()
8866                .unwrap()
8867                .deployment_manifest,
8868        )
8869        .await
8870        .unwrap_err();
8871
8872        assert!(
8873            error
8874                .to_string()
8875                .contains("did not come from expected pool"),
8876            "was: {error}"
8877        );
8878    }
8879
8880    #[rstest]
8881    fn verified_sell_quote_sets_signed_amounts() {
8882        let plan = fixture_sell_plan();
8883        let quote = UniswapV3Quote {
8884            amount: expected_sell_quote_amount(),
8885            sqrt_price_x96_after: U160::from(1u128 << 96),
8886            initialized_ticks_crossed: 0,
8887            gas_estimate: U256::from(50_000u64),
8888        };
8889
8890        let (amount_in, min_amount_out) = verified_swap_amounts(&plan, quote).unwrap();
8891
8892        assert_eq!(amount_in, U256::from(1_000_000_000_000_000u64));
8893        assert_eq!(min_amount_out, expected_min_amount_out(50));
8894    }
8895
8896    #[rstest]
8897    fn exact_output_amount_extracts_negative_leg() {
8898        let pool = test_pool();
8899        let profiler = test_profiler(&pool, FIXTURE_BLOCK);
8900        let quote = profiler
8901            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
8902            .unwrap();
8903
8904        let amount = exact_output_amount(&quote, true).unwrap();
8905
8906        assert!(amount < U256::from(1_000_000_000_000_000u64));
8907        assert!(amount > U256::from(990_000_000_000_000u64));
8908
8909        let error = exact_output_amount(&quote, false).unwrap_err();
8910        assert!(
8911            error.to_string().contains("is not a positive output"),
8912            "was: {error}"
8913        );
8914    }
8915
8916    #[rstest]
8917    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV3", true)]
8918    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3", false)]
8919    #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4", false)]
8920    #[case("ETHUSDT-PERP.BINANCE", false)]
8921    fn handles_order_venue_matches_chain_and_dex(#[case] instrument: &str, #[case] expected: bool) {
8922        let client = test_client("http://127.0.0.1:1".to_string());
8923        let instrument_id: InstrumentId = instrument.parse().unwrap();
8924
8925        assert_eq!(client.handles_order_venue(instrument_id.venue), expected);
8926    }
8927
8928    #[tokio::test]
8929    async fn submit_order_denies_buy_without_allowlisted_pair() {
8930        let (mut client, cache) =
8931            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
8932        let pool = test_pool();
8933        let order = test_market_buy_order(pool.instrument_id);
8934        cache
8935            .borrow_mut()
8936            .add_order(order.clone(), None, None, true)
8937            .unwrap();
8938        let mut receiver = start_with_events(&mut client);
8939
8940        client.submit_order(submit_order_cmd(&order)).unwrap();
8941
8942        let events = collect_order_events(&mut receiver);
8943        assert_eq!(events.len(), 1);
8944        let OrderEventAny::Denied(denied) = &events[0] else {
8945            panic!("expected OrderDenied, was {:?}", events[0]);
8946        };
8947        assert!(
8948            denied
8949                .reason
8950                .as_str()
8951                .contains("not in the `allowed_token_pairs` allowlist"),
8952            "was: {}",
8953            denied.reason
8954        );
8955        assert!(
8956            denied.reason.as_str().contains(&USDC_ADDRESS.to_string()),
8957            "was: {}",
8958            denied.reason
8959        );
8960    }
8961
8962    #[tokio::test]
8963    async fn submit_order_denies_buy_quote_denominated_quantity() {
8964        let (mut client, cache) =
8965            swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8966        let pool = test_pool();
8967        let order = OrderTestBuilder::new(OrderType::Market)
8968            .trader_id(TraderId::from("TRADER-001"))
8969            .strategy_id(StrategyId::from("S-001"))
8970            .instrument_id(pool.instrument_id)
8971            .client_order_id(ClientOrderId::from("O-SWAP-BUY-001"))
8972            .side(OrderSide::Buy)
8973            .quantity(Quantity::from("0.001"))
8974            .quote_quantity(true)
8975            .build();
8976        cache
8977            .borrow_mut()
8978            .add_order(order.clone(), None, None, true)
8979            .unwrap();
8980        let mut receiver = start_with_events(&mut client);
8981
8982        client.submit_order(submit_order_cmd(&order)).unwrap();
8983
8984        let events = collect_order_events(&mut receiver);
8985        assert_eq!(events.len(), 1);
8986        let OrderEventAny::Denied(denied) = &events[0] else {
8987            panic!("expected OrderDenied, was {:?}", events[0]);
8988        };
8989        assert!(
8990            denied.reason.as_str().contains("Quote-denominated"),
8991            "was: {}",
8992            denied.reason
8993        );
8994    }
8995
8996    #[tokio::test]
8997    async fn submit_order_denies_buy_amount_above_max_order_amount() {
8998        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8999        config.max_order_amount = Some(999_999_999_999_999);
9000        let (mut client, cache) = swap_client_with_cache(config);
9001        let order = test_market_buy_order(test_pool().instrument_id);
9002        cache
9003            .borrow_mut()
9004            .add_order(order.clone(), None, None, true)
9005            .unwrap();
9006        let mut receiver = start_with_events(&mut client);
9007
9008        client.submit_order(submit_order_cmd(&order)).unwrap();
9009
9010        let events = collect_order_events(&mut receiver);
9011        assert_eq!(events.len(), 1);
9012        let OrderEventAny::Denied(denied) = &events[0] else {
9013            panic!("expected OrderDenied, was {:?}", events[0]);
9014        };
9015        assert!(
9016            denied
9017                .reason
9018                .as_str()
9019                .contains("exceeds the configured `max_order_amount`"),
9020            "was: {}",
9021            denied.reason
9022        );
9023    }
9024
9025    #[tokio::test]
9026    async fn submit_order_denies_buy_without_quote_spend_limit() {
9027        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
9028        config.quote_spend_limits = None;
9029        let (mut client, cache) = swap_client_with_cache(config);
9030        let order = test_market_buy_order(test_pool().instrument_id);
9031        cache
9032            .borrow_mut()
9033            .add_order(order.clone(), None, None, true)
9034            .unwrap();
9035        let mut receiver = start_with_events(&mut client);
9036
9037        client.submit_order(submit_order_cmd(&order)).unwrap();
9038
9039        let events = collect_order_events(&mut receiver);
9040        assert_eq!(events.len(), 1);
9041        let OrderEventAny::Denied(denied) = &events[0] else {
9042            panic!("expected OrderDenied, was {:?}", events[0]);
9043        };
9044        assert!(
9045            denied
9046                .reason
9047                .as_str()
9048                .contains("No `quote_spend_limits` entry for BUY token pair"),
9049            "was: {}",
9050            denied.reason
9051        );
9052    }
9053
9054    #[tokio::test]
9055    async fn submit_order_uses_pair_specific_quote_spend_limit() {
9056        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
9057        config.quote_spend_limits = Some(vec![quote_spend_limit(
9058            WETH,
9059            USDC,
9060            18,
9061            "1000000000000000000",
9062        )]);
9063        let (mut client, cache) = swap_client_with_cache(config);
9064        let order = test_market_buy_order(test_pool().instrument_id);
9065        cache
9066            .borrow_mut()
9067            .add_order(order.clone(), None, None, true)
9068            .unwrap();
9069        let mut receiver = start_with_events(&mut client);
9070
9071        client.submit_order(submit_order_cmd(&order)).unwrap();
9072
9073        let events = collect_order_events(&mut receiver);
9074        assert_eq!(events.len(), 1);
9075        let OrderEventAny::Denied(denied) = &events[0] else {
9076            panic!("expected OrderDenied, was {:?}", events[0]);
9077        };
9078        assert!(
9079            denied
9080                .reason
9081                .as_str()
9082                .contains("No `quote_spend_limits` entry for BUY token pair"),
9083            "was: {}",
9084            denied.reason
9085        );
9086    }
9087
9088    #[tokio::test]
9089    async fn submit_order_denies_buy_with_quote_spend_precision_mismatch() {
9090        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
9091        config.quote_spend_limits.as_mut().unwrap()[0].spend_token_decimals = 18;
9092        let error = test_client_result(config, test_pool()).unwrap_err();
9093        assert!(
9094            error
9095                .to_string()
9096                .contains("Quote spend limit decimals do not match the deployment manifest"),
9097            "was: {error}"
9098        );
9099    }
9100
9101    #[tokio::test]
9102    async fn submit_order_denies_buy_with_zero_quote_spend_limit() {
9103        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
9104        config.quote_spend_limits.as_mut().unwrap()[0].max_amount = "0".to_string();
9105        let (mut client, cache) = swap_client_with_cache(config);
9106        let order = test_market_buy_order(test_pool().instrument_id);
9107        cache
9108            .borrow_mut()
9109            .add_order(order.clone(), None, None, true)
9110            .unwrap();
9111        let mut receiver = start_with_events(&mut client);
9112
9113        client.submit_order(submit_order_cmd(&order)).unwrap();
9114
9115        let events = collect_order_events(&mut receiver);
9116        assert_eq!(events.len(), 1);
9117        let OrderEventAny::Denied(denied) = &events[0] else {
9118            panic!("expected OrderDenied, was {:?}", events[0]);
9119        };
9120        assert!(
9121            denied
9122                .reason
9123                .as_str()
9124                .contains("exceeds the configured `quote_spend_limits` maximum 0"),
9125            "was: {}",
9126            denied.reason
9127        );
9128    }
9129
9130    #[tokio::test]
9131    async fn submit_order_denies_buy_one_raw_unit_above_quote_spend_limit_before_readiness() {
9132        let amount_in = expected_buy_amount_in();
9133        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
9134        config.quote_spend_limits.as_mut().unwrap()[0].max_amount =
9135            (amount_in - U256::from(1u8)).to_string();
9136        let (mut client, cache) = swap_client_with_cache(config);
9137        let order = test_market_buy_order(test_pool().instrument_id);
9138        cache
9139            .borrow_mut()
9140            .add_order(order.clone(), None, None, true)
9141            .unwrap();
9142        let mut receiver = start_with_events(&mut client);
9143
9144        client.submit_order(submit_order_cmd(&order)).unwrap();
9145
9146        let events = collect_order_events(&mut receiver);
9147        assert_eq!(events.len(), 1);
9148        let OrderEventAny::Denied(denied) = &events[0] else {
9149            panic!("expected OrderDenied, was {:?}", events[0]);
9150        };
9151        assert!(
9152            denied.reason.as_str().contains(&format!(
9153                "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits`"
9154            )),
9155            "was: {}",
9156            denied.reason
9157        );
9158        assert!(!client.core.is_connected());
9159        assert!(!client.cache.has_database());
9160        assert!(client.signer.is_none());
9161        assert!(client.pending_tasks.is_empty());
9162    }
9163
9164    #[tokio::test]
9165    async fn submit_order_denies_non_market_order_type() {
9166        let (mut client, cache) =
9167            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9168        let pool = test_pool();
9169        let order = OrderTestBuilder::new(OrderType::Limit)
9170            .trader_id(TraderId::from("TRADER-001"))
9171            .strategy_id(StrategyId::from("S-001"))
9172            .instrument_id(pool.instrument_id)
9173            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9174            .side(OrderSide::Sell)
9175            .quantity(Quantity::from("0.001"))
9176            .price(Price::from("2000"))
9177            .build();
9178        cache
9179            .borrow_mut()
9180            .add_order(order.clone(), None, None, true)
9181            .unwrap();
9182        let mut receiver = start_with_events(&mut client);
9183
9184        client.submit_order(submit_order_cmd(&order)).unwrap();
9185
9186        let events = collect_order_events(&mut receiver);
9187        assert_eq!(events.len(), 1);
9188        let OrderEventAny::Denied(denied) = &events[0] else {
9189            panic!("expected OrderDenied, was {:?}", events[0]);
9190        };
9191        assert!(
9192            denied.reason.as_str().contains("only Market is supported"),
9193            "was: {}",
9194            denied.reason
9195        );
9196    }
9197
9198    #[tokio::test]
9199    async fn submit_order_denies_quote_denominated_quantity() {
9200        let (mut client, cache) =
9201            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9202        let pool = test_pool();
9203        let order = OrderTestBuilder::new(OrderType::Market)
9204            .trader_id(TraderId::from("TRADER-001"))
9205            .strategy_id(StrategyId::from("S-001"))
9206            .instrument_id(pool.instrument_id)
9207            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9208            .side(OrderSide::Sell)
9209            .quantity(Quantity::from("0.001"))
9210            .quote_quantity(true)
9211            .build();
9212        cache
9213            .borrow_mut()
9214            .add_order(order.clone(), None, None, true)
9215            .unwrap();
9216        let mut receiver = start_with_events(&mut client);
9217
9218        client.submit_order(submit_order_cmd(&order)).unwrap();
9219
9220        let events = collect_order_events(&mut receiver);
9221        assert_eq!(events.len(), 1);
9222        let OrderEventAny::Denied(denied) = &events[0] else {
9223            panic!("expected OrderDenied, was {:?}", events[0]);
9224        };
9225        assert!(
9226            denied.reason.as_str().contains("Quote-denominated"),
9227            "was: {}",
9228            denied.reason
9229        );
9230    }
9231
9232    #[tokio::test]
9233    async fn submit_order_denies_unknown_pool() {
9234        let (mut client, cache) =
9235            swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9236        let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
9237            .parse()
9238            .unwrap();
9239        let order = OrderTestBuilder::new(OrderType::Market)
9240            .trader_id(TraderId::from("TRADER-001"))
9241            .strategy_id(StrategyId::from("S-001"))
9242            .instrument_id(unknown)
9243            .client_order_id(ClientOrderId::from("O-SWAP-001"))
9244            .side(OrderSide::Sell)
9245            .quantity(Quantity::from("0.001"))
9246            .build();
9247        cache
9248            .borrow_mut()
9249            .add_order(order.clone(), None, None, true)
9250            .unwrap();
9251        let mut receiver = start_with_events(&mut client);
9252
9253        client.submit_order(submit_order_cmd(&order)).unwrap();
9254
9255        let events = collect_order_events(&mut receiver);
9256        assert_eq!(events.len(), 1);
9257        let OrderEventAny::Denied(denied) = &events[0] else {
9258            panic!("expected OrderDenied, was {:?}", events[0]);
9259        };
9260        assert!(
9261            denied.reason.as_str().contains("Unknown pool"),
9262            "was: {}",
9263            denied.reason
9264        );
9265    }
9266
9267    #[tokio::test]
9268    async fn submit_order_denies_sell_when_only_buy_pair_allowlisted() {
9269        let mut config = test_config("http://127.0.0.1:1".to_string());
9270        config.allowed_token_pairs = Some(vec![(USDC.to_string(), WETH.to_string())]);
9271        let (mut client, _) = swap_client_with_cache(config);
9272        let order = test_market_sell_order(test_pool().instrument_id);
9273        let mut receiver = start_with_events(&mut client);
9274
9275        client.submit_order(submit_order_cmd(&order)).unwrap();
9276
9277        let events = collect_order_events(&mut receiver);
9278        assert_eq!(events.len(), 1);
9279        let OrderEventAny::Denied(denied) = &events[0] else {
9280            panic!("expected OrderDenied, was {:?}", events[0]);
9281        };
9282        assert!(
9283            denied
9284                .reason
9285                .as_str()
9286                .contains("not in the `allowed_token_pairs` allowlist"),
9287            "was: {}",
9288            denied.reason
9289        );
9290        assert!(
9291            denied.reason.as_str().contains(&WETH_ADDRESS.to_string()),
9292            "was: {}",
9293            denied.reason
9294        );
9295    }
9296
9297    #[tokio::test]
9298    async fn submit_order_sell_ignores_quote_spend_limits() {
9299        let mut config = test_config("http://127.0.0.1:1".to_string());
9300        config.quote_spend_limits = Some(vec![quote_spend_limit(WETH, USDC, 18, "0")]);
9301        let (mut client, _) = swap_client_with_cache(config);
9302        let order = test_market_sell_order(test_pool().instrument_id);
9303        let mut receiver = start_with_events(&mut client);
9304
9305        client.submit_order(submit_order_cmd(&order)).unwrap();
9306
9307        let events = collect_order_events(&mut receiver);
9308        assert_eq!(events.len(), 1);
9309        let OrderEventAny::Denied(denied) = &events[0] else {
9310            panic!("expected OrderDenied, was {:?}", events[0]);
9311        };
9312        assert!(
9313            denied
9314                .reason
9315                .as_str()
9316                .contains("Blockchain execution client is not connected"),
9317            "was: {}",
9318            denied.reason
9319        );
9320    }
9321
9322    #[tokio::test]
9323    async fn submit_order_denies_token_pair_outside_allowlist() {
9324        let mut config = test_config("http://127.0.0.1:1".to_string());
9325        config.allowed_token_pairs = Some(Vec::new());
9326        let (mut client, _) = swap_client_with_cache(config);
9327        let order = test_market_sell_order(test_pool().instrument_id);
9328        let mut receiver = start_with_events(&mut client);
9329
9330        client.submit_order(submit_order_cmd(&order)).unwrap();
9331
9332        let events = collect_order_events(&mut receiver);
9333        assert_eq!(events.len(), 1);
9334        let OrderEventAny::Denied(denied) = &events[0] else {
9335            panic!("expected OrderDenied, was {:?}", events[0]);
9336        };
9337        assert!(
9338            denied
9339                .reason
9340                .as_str()
9341                .contains("not in the `allowed_token_pairs` allowlist"),
9342            "was: {}",
9343            denied.reason
9344        );
9345    }
9346
9347    #[tokio::test]
9348    async fn submit_order_denies_amount_above_max_order_amount() {
9349        let mut config = test_config("http://127.0.0.1:1".to_string());
9350        config.max_order_amount = Some(999_999_999_999_999); // 0.001 WETH in raw units minus one
9351        let (mut client, _) = swap_client_with_cache(config);
9352        let order = test_market_sell_order(test_pool().instrument_id);
9353        let mut receiver = start_with_events(&mut client);
9354
9355        client.submit_order(submit_order_cmd(&order)).unwrap();
9356
9357        let events = collect_order_events(&mut receiver);
9358        assert_eq!(events.len(), 1);
9359        let OrderEventAny::Denied(denied) = &events[0] else {
9360            panic!("expected OrderDenied, was {:?}", events[0]);
9361        };
9362        assert!(
9363            denied
9364                .reason
9365                .as_str()
9366                .contains("exceeds the configured `max_order_amount`"),
9367            "was: {}",
9368            denied.reason
9369        );
9370    }
9371
9372    #[tokio::test]
9373    async fn submit_order_denies_slippage_param_above_ceiling() {
9374        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9375        let order = test_market_sell_order(test_pool().instrument_id);
9376        let mut cmd = submit_order_cmd(&order);
9377        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 201}"#).unwrap());
9378        let mut receiver = start_with_events(&mut client);
9379
9380        client.submit_order(cmd).unwrap();
9381
9382        let events = collect_order_events(&mut receiver);
9383        assert_eq!(events.len(), 1);
9384        let OrderEventAny::Denied(denied) = &events[0] else {
9385            panic!("expected OrderDenied, was {:?}", events[0]);
9386        };
9387        assert!(
9388            denied
9389                .reason
9390                .as_str()
9391                .contains("exceeds the configured `max_slippage_bps`"),
9392            "was: {}",
9393            denied.reason
9394        );
9395    }
9396
9397    #[tokio::test]
9398    async fn submit_order_denies_pool_without_fee_tier() {
9399        let addr = start_mock_rpc_server(MockRpcState::default()).await;
9400        let mut pool = test_pool();
9401        pool.fee = None;
9402        let cache = Rc::new(RefCell::new(Cache::default()));
9403        cache.borrow_mut().add_pool(pool.clone()).unwrap();
9404        let order = test_market_sell_order(pool.instrument_id);
9405        cache
9406            .borrow_mut()
9407            .add_order(order.clone(), None, None, false)
9408            .unwrap();
9409        let core = ExecutionClientCore::new(
9410            TraderId::from("TRADER-001"),
9411            ClientId::from("BLOCKCHAIN-001"),
9412            *BLOCKCHAIN_VENUE,
9413            OmsType::Netting,
9414            AccountId::from("BLOCKCHAIN-001"),
9415            AccountType::Wallet,
9416            None,
9417            cache,
9418        );
9419        let mut client =
9420            BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9421        let mut receiver = start_with_events(&mut client);
9422
9423        client.submit_order(submit_order_cmd(&order)).unwrap();
9424
9425        let events = collect_order_events(&mut receiver);
9426        assert_eq!(events.len(), 1);
9427        let OrderEventAny::Denied(denied) = &events[0] else {
9428            panic!("expected OrderDenied, was {:?}", events[0]);
9429        };
9430        assert!(
9431            denied.reason.as_str().contains("no fee tier"),
9432            "was: {}",
9433            denied.reason
9434        );
9435    }
9436
9437    #[tokio::test]
9438    async fn submit_order_denies_without_live_profiler() {
9439        let addr = start_mock_rpc_server(MockRpcState::default()).await;
9440        let cache = Rc::new(RefCell::new(Cache::default()));
9441        let pool = test_pool();
9442        cache.borrow_mut().add_pool(pool.clone()).unwrap();
9443        let order = test_market_sell_order(pool.instrument_id);
9444        cache
9445            .borrow_mut()
9446            .add_order(order.clone(), None, None, false)
9447            .unwrap();
9448        let core = ExecutionClientCore::new(
9449            TraderId::from("TRADER-001"),
9450            ClientId::from("BLOCKCHAIN-001"),
9451            *BLOCKCHAIN_VENUE,
9452            OmsType::Netting,
9453            AccountId::from("BLOCKCHAIN-001"),
9454            AccountType::Wallet,
9455            None,
9456            cache,
9457        );
9458        let mut client =
9459            BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
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.as_str().contains("No pool profiler"),
9471            "was: {}",
9472            denied.reason
9473        );
9474    }
9475
9476    #[tokio::test]
9477    async fn submit_order_denies_when_not_connected() {
9478        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9479        let order = test_market_sell_order(test_pool().instrument_id);
9480        let mut receiver = start_with_events(&mut client);
9481
9482        client.submit_order(submit_order_cmd(&order)).unwrap();
9483
9484        let events = collect_order_events(&mut receiver);
9485        assert_eq!(events.len(), 1);
9486        let OrderEventAny::Denied(denied) = &events[0] else {
9487            panic!("expected OrderDenied, was {:?}", events[0]);
9488        };
9489        assert!(
9490            denied.reason.as_str().contains("is not connected"),
9491            "was: {}",
9492            denied.reason
9493        );
9494    }
9495
9496    #[tokio::test]
9497    async fn submit_order_denies_without_durable_store() {
9498        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9499        client.core.set_connected();
9500        let order = test_market_sell_order(test_pool().instrument_id);
9501        let mut receiver = start_with_events(&mut client);
9502
9503        client.submit_order(submit_order_cmd(&order)).unwrap();
9504
9505        let events = collect_order_events(&mut receiver);
9506        assert_eq!(events.len(), 1);
9507        let OrderEventAny::Denied(denied) = &events[0] else {
9508            panic!("expected OrderDenied, was {:?}", events[0]);
9509        };
9510        assert!(
9511            denied
9512                .reason
9513                .as_str()
9514                .contains("No durable store configured"),
9515            "was: {}",
9516            denied.reason
9517        );
9518    }
9519
9520    #[tokio::test]
9521    async fn submit_order_denies_when_transaction_in_flight() {
9522        let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9523        client.core.set_connected();
9524        *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
9525            intent_id: 1,
9526            nonce: 7,
9527            tx_hash: B256::ZERO,
9528            purpose: TransactionPurpose::Wrap,
9529        }));
9530        let order = test_market_sell_order(test_pool().instrument_id);
9531        let mut receiver = start_with_events(&mut client);
9532
9533        client.submit_order(submit_order_cmd(&order)).unwrap();
9534
9535        let events = collect_order_events(&mut receiver);
9536        assert_eq!(events.len(), 1);
9537        let OrderEventAny::Denied(denied) = &events[0] else {
9538            panic!("expected OrderDenied, was {:?}", events[0]);
9539        };
9540        assert!(
9541            denied.reason.as_str().contains("still awaiting finality"),
9542            "was: {}",
9543            denied.reason
9544        );
9545    }
9546
9547    #[tokio::test]
9548    async fn submit_order_denies_without_signer() {
9549        let Some((admin_pool, schema, mut client, _, _)) =
9550            swap_client_with_database("execution_submit_no_signer_test", swap_rpc_state().await)
9551                .await
9552        else {
9553            return;
9554        };
9555        client.signer = None;
9556        let order = test_market_sell_order(test_pool().instrument_id);
9557        let mut receiver = start_with_events(&mut client);
9558
9559        client.submit_order(submit_order_cmd(&order)).unwrap();
9560
9561        let events = collect_order_events(&mut receiver);
9562        assert_eq!(events.len(), 1);
9563        let OrderEventAny::Denied(denied) = &events[0] else {
9564            panic!("expected OrderDenied, was {:?}", events[0]);
9565        };
9566        assert!(
9567            denied.reason.as_str().contains("Signer not initialized"),
9568            "was: {}",
9569            denied.reason
9570        );
9571
9572        drop_execution_schema(&admin_pool, &schema).await;
9573    }
9574
9575    #[tokio::test]
9576    async fn submit_order_broadcasts_swap_and_records_client_order_id() {
9577        let Some((admin_pool, schema, mut client, state, _)) =
9578            swap_client_with_database("execution_submit_success_test", swap_rpc_state().await)
9579                .await
9580        else {
9581            return;
9582        };
9583        let order = test_market_sell_order(test_pool().instrument_id);
9584        let mut receiver = start_with_events(&mut client);
9585        let expected_min_out = expected_min_amount_out(50);
9586
9587        client.submit_order(submit_order_cmd(&order)).unwrap();
9588        await_pending_tasks(&client).await;
9589
9590        let events = collect_order_events(&mut receiver);
9591        assert_swap_submitted_and_filled(&events);
9592        let OrderEventAny::Submitted(submitted) = &events[0] else {
9593            panic!("expected OrderSubmitted, was {:?}", events[0]);
9594        };
9595        assert_eq!(submitted.client_order_id, order.client_order_id());
9596
9597        // The recorded broadcast matches the fully signed expected transaction
9598        let (expected_hash, expected_raw) = expected_swap_tx(expected_min_out).await;
9599        let broadcasts: Vec<_> = state
9600            .recorded_requests()
9601            .into_iter()
9602            .filter(|request| request["method"] == "eth_sendRawTransaction")
9603            .collect();
9604        assert_eq!(broadcasts.len(), 1);
9605        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
9606
9607        let record = client
9608            .cache
9609            .get_execution_transaction(42161, &expected_hash.to_string())
9610            .await
9611            .unwrap()
9612            .unwrap();
9613        assert_eq!(record.nonce, 7);
9614        assert_eq!(record.purpose, "swap");
9615        assert_eq!(record.status, "finalized");
9616        assert_eq!(
9617            record.client_order_id.as_deref(),
9618            Some(order.client_order_id().as_str())
9619        );
9620        assert!(client.in_flight.lock().is_none());
9621        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
9622            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
9623        )))
9624        .fetch_one(&admin_pool)
9625        .await
9626        .unwrap();
9627        let decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9628            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9629             WHERE outcome = 'verified'"
9630        )))
9631        .fetch_one(&admin_pool)
9632        .await
9633        .unwrap();
9634        let connect_decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9635            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9636                 WHERE decision_class = 'connect' AND outcome = 'verified'"
9637        )))
9638        .fetch_one(&admin_pool)
9639        .await
9640        .unwrap();
9641        assert_eq!(nonce_state, (8, 1));
9642        assert_eq!(decision_count, 35);
9643        assert_eq!(connect_decision_count, 1);
9644
9645        drop_execution_schema(&admin_pool, &schema).await;
9646    }
9647
9648    #[tokio::test]
9649    async fn submit_order_pins_every_pre_sign_state_read_to_swap_anchor() {
9650        let Some((admin_pool, schema, mut client, state, _)) =
9651            swap_client_with_database("execution_submit_anchor_reads_test", swap_rpc_state().await)
9652                .await
9653        else {
9654            return;
9655        };
9656        let order = test_market_sell_order(test_pool().instrument_id);
9657        let mut receiver = start_with_events(&mut client);
9658
9659        client.submit_order(submit_order_cmd(&order)).unwrap();
9660        await_pending_tasks(&client).await;
9661
9662        let events = collect_order_events(&mut receiver);
9663        assert_swap_submitted_and_filled(&events);
9664        let requests = state.recorded_requests();
9665        let broadcast_index = requests
9666            .iter()
9667            .position(|request| request["method"] == "eth_sendRawTransaction")
9668            .unwrap();
9669        let pre_broadcast = &requests[..broadcast_index];
9670        let latest_reads: Vec<_> = pre_broadcast
9671            .iter()
9672            .filter(|request| {
9673                request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9674            })
9675            .collect();
9676        assert_eq!(latest_reads.len(), 3);
9677        assert!(
9678            latest_reads
9679                .iter()
9680                .all(|request| request["params"] == serde_json::json!(["latest", false]))
9681        );
9682
9683        for method in [
9684            "eth_getCode",
9685            "eth_call",
9686            "eth_estimateGas",
9687            "eth_getBalance",
9688        ] {
9689            let pinned: Vec<_> = pre_broadcast
9690                .iter()
9691                .filter(|request| request["method"] == method)
9692                .collect();
9693            assert!(!pinned.is_empty(), "method {method}");
9694            assert_eq!(pinned.len() % 3, 0, "method {method}: {pinned:?}");
9695            assert!(
9696                pinned
9697                    .iter()
9698                    .all(|request| request["params"][1] == FIXTURE_BLOCK_PARAM),
9699                "method {method}: {pinned:?}"
9700            );
9701        }
9702
9703        let numbered_blocks: Vec<_> = pre_broadcast
9704            .iter()
9705            .filter(|request| {
9706                request["method"] == "eth_getBlockByNumber"
9707                    && request["params"][0] == FIXTURE_BLOCK_PARAM
9708            })
9709            .collect();
9710        assert!(!numbered_blocks.is_empty());
9711        assert_eq!(numbered_blocks.len() % 3, 0);
9712        assert!(
9713            numbered_blocks
9714                .iter()
9715                .all(|request| request["params"][1] == false)
9716        );
9717
9718        let chain_ids: Vec<_> = pre_broadcast
9719            .iter()
9720            .filter(|request| request["method"] == "eth_chainId")
9721            .collect();
9722        assert_eq!(chain_ids.len(), 3);
9723        assert!(
9724            chain_ids
9725                .iter()
9726                .all(|request| request["params"] == serde_json::json!([]))
9727        );
9728        let nonces: Vec<_> = pre_broadcast
9729            .iter()
9730            .filter(|request| request["method"] == "eth_getTransactionCount")
9731            .collect();
9732        assert_eq!(nonces.len(), 6);
9733        assert_eq!(
9734            nonces
9735                .iter()
9736                .filter(|request| {
9737                    request["params"] == serde_json::json!([WALLET.to_ascii_lowercase(), "pending"])
9738                })
9739                .count(),
9740            3
9741        );
9742        assert_eq!(
9743            nonces
9744                .iter()
9745                .filter(|request| {
9746                    request["params"]
9747                        == serde_json::json!([WALLET.to_ascii_lowercase(), FIXTURE_BLOCK_PARAM])
9748                })
9749                .count(),
9750            3
9751        );
9752        let priority_fees: Vec<_> = pre_broadcast
9753            .iter()
9754            .filter(|request| request["method"] == "eth_maxPriorityFeePerGas")
9755            .collect();
9756        assert_eq!(priority_fees.len(), 3);
9757        assert!(
9758            priority_fees
9759                .iter()
9760                .all(|request| request["params"] == serde_json::json!([]))
9761        );
9762
9763        let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
9764        assert_eq!(
9765            requests[broadcast_index]["params"][0].as_str().unwrap(),
9766            expected_raw
9767        );
9768        let created_block: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9769            "SELECT created_block FROM {schema}.execution_intent"
9770        )))
9771        .fetch_one(&admin_pool)
9772        .await
9773        .unwrap();
9774        assert_eq!(created_block, i64::try_from(FIXTURE_BLOCK).unwrap());
9775
9776        drop_execution_schema(&admin_pool, &schema).await;
9777    }
9778
9779    #[tokio::test]
9780    async fn submit_order_denies_changed_swap_anchor_before_signing() {
9781        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9782        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9783        let state = swap_rpc_state().await.with_parameter_response_sequence(
9784            "eth_getBlockByNumber",
9785            FIXTURE_BLOCK_PARAM,
9786            &[
9787                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9788                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9789                &canonical, &changed, &changed, &changed,
9790            ],
9791        );
9792        let Some((admin_pool, schema, mut client, state, _)) =
9793            swap_client_with_database("execution_submit_anchor_change_test", state).await
9794        else {
9795            return;
9796        };
9797        let order = test_market_sell_order(test_pool().instrument_id);
9798        let mut receiver = start_with_events(&mut client);
9799
9800        client.submit_order(submit_order_cmd(&order)).unwrap();
9801        await_pending_tasks(&client).await;
9802
9803        let events = collect_order_events(&mut receiver);
9804        assert_eq!(events.len(), 1, "was: {events:?}");
9805        let OrderEventAny::Denied(denied) = &events[0] else {
9806            panic!("expected OrderDenied, was {:?}", events[0]);
9807        };
9808        assert!(
9809            denied
9810                .reason
9811                .as_str()
9812                .contains("pre-sign checkpoint reread verification disagreed"),
9813            "was: {}",
9814            denied.reason
9815        );
9816        assert_eq!(
9817            execution_intent_markers(&admin_pool, &schema).await,
9818            vec![("swap".into(), "recoverable".into(), false, false)]
9819        );
9820        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9821            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9822        )))
9823        .fetch_one(&admin_pool)
9824        .await
9825        .unwrap();
9826        assert_eq!(signed_count, 0);
9827        assert!(
9828            state
9829                .recorded_requests()
9830                .iter()
9831                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9832        );
9833        assert!(client.in_flight.lock().is_none());
9834
9835        drop_execution_schema(&admin_pool, &schema).await;
9836    }
9837
9838    #[tokio::test]
9839    async fn submit_order_keeps_verified_swap_anchor_when_latest_advances() {
9840        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9841        let newer = fixture_block_response(FIXTURE_BLOCK + 1, B256::from([0x44; 32]));
9842        let state = swap_rpc_state().await.with_parameter_response_sequence(
9843            "eth_getBlockByNumber",
9844            "latest",
9845            &[&canonical, &canonical, &canonical, &newer, &newer, &newer],
9846        );
9847        let Some((admin_pool, schema, mut client, state, _)) =
9848            swap_client_with_database("execution_submit_advancing_head_test", state).await
9849        else {
9850            return;
9851        };
9852        let order = test_market_sell_order(test_pool().instrument_id);
9853        let mut receiver = start_with_events(&mut client);
9854
9855        client.submit_order(submit_order_cmd(&order)).unwrap();
9856        await_pending_tasks(&client).await;
9857
9858        let events = collect_order_events(&mut receiver);
9859        assert_swap_submitted_and_filled(&events);
9860        let latest_reads = state
9861            .recorded_requests()
9862            .iter()
9863            .filter(|request| {
9864                request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9865            })
9866            .count();
9867        assert_eq!(latest_reads, 3);
9868
9869        drop_execution_schema(&admin_pool, &schema).await;
9870    }
9871
9872    #[tokio::test]
9873    async fn submit_order_denies_changed_quote_watermark_before_signing() {
9874        let watermark_number = FIXTURE_BLOCK;
9875        let watermark_hash = B256::from([0x11; 32]);
9876        let canonical = fixture_block_response(watermark_number, watermark_hash);
9877        let changed = fixture_block_response(watermark_number, B256::from([0x66; 32]));
9878        let watermark_param = format!("0x{watermark_number:x}");
9879        let min_amount_out = expected_min_amount_out(50);
9880        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
9881        let head = finalized_swap_block(tx_hash, min_amount_out);
9882        let state = swap_rpc_state()
9883            .await
9884            .with_response("eth_getBlockByNumber", &head)
9885            .with_parameter_response_sequence(
9886                "eth_getBlockByNumber",
9887                &watermark_param,
9888                &[
9889                    &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9890                    &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9891                    &changed, &changed, &changed,
9892                ],
9893            );
9894        let Some((admin_pool, schema, mut client, state, cache)) =
9895            swap_client_with_database("execution_submit_watermark_change_test", state).await
9896        else {
9897            return;
9898        };
9899        let pool = test_pool();
9900        cache
9901            .borrow_mut()
9902            .add_pool_profiler(test_profiler_at_block(
9903                &pool,
9904                watermark_number,
9905                &watermark_hash.to_string(),
9906            ))
9907            .unwrap();
9908        let order = test_market_sell_order(pool.instrument_id);
9909        let mut receiver = start_with_events(&mut client);
9910
9911        client.submit_order(submit_order_cmd(&order)).unwrap();
9912        await_pending_tasks(&client).await;
9913
9914        let events = collect_order_events(&mut receiver);
9915        assert_eq!(events.len(), 1, "was: {events:?}");
9916        let OrderEventAny::Denied(denied) = &events[0] else {
9917            panic!("expected OrderDenied, was {:?}", events[0]);
9918        };
9919        assert!(
9920            denied.reason.as_str().contains("changed before signing"),
9921            "was: {}",
9922            denied.reason
9923        );
9924        assert_eq!(
9925            execution_intent_markers(&admin_pool, &schema).await,
9926            vec![("swap".into(), "recoverable".into(), false, false)]
9927        );
9928        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9929            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9930        )))
9931        .fetch_one(&admin_pool)
9932        .await
9933        .unwrap();
9934        assert_eq!(signed_count, 0);
9935        assert!(
9936            state
9937                .recorded_requests()
9938                .iter()
9939                .all(|request| { request["method"] != "eth_sendRawTransaction" })
9940        );
9941        assert!(client.in_flight.lock().is_none());
9942
9943        drop_execution_schema(&admin_pool, &schema).await;
9944    }
9945
9946    #[tokio::test]
9947    async fn changed_swap_anchor_retains_ownership_when_recovery_commit_fails() {
9948        let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9949        let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9950        let state = swap_rpc_state().await.with_parameter_response_sequence(
9951            "eth_getBlockByNumber",
9952            FIXTURE_BLOCK_PARAM,
9953            &[
9954                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9955                &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9956                &canonical, &changed, &changed, &changed,
9957            ],
9958        );
9959        let Some((admin_pool, schema, mut client, state, _)) =
9960            swap_client_with_database("execution_submit_anchor_recovery_fail_test", state).await
9961        else {
9962            return;
9963        };
9964        install_recoverable_commit_rejection(&admin_pool, &schema).await;
9965        let order = test_market_sell_order(test_pool().instrument_id);
9966        let mut receiver = start_with_events(&mut client);
9967
9968        client.submit_order(submit_order_cmd(&order)).unwrap();
9969        await_pending_tasks(&client).await;
9970
9971        let events = collect_order_events(&mut receiver);
9972        assert_eq!(events.len(), 1, "was: {events:?}");
9973        let OrderEventAny::Denied(denied) = &events[0] else {
9974            panic!("expected OrderDenied, was {:?}", events[0]);
9975        };
9976        assert!(
9977            denied
9978                .reason
9979                .as_str()
9980                .contains("pre-sign checkpoint reread verification disagreed"),
9981            "was: {}",
9982            denied.reason
9983        );
9984        assert_eq!(
9985            execution_intent_markers(&admin_pool, &schema).await,
9986            vec![("swap".into(), "prepared".into(), false, true)]
9987        );
9988        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9989            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9990        )))
9991        .fetch_one(&admin_pool)
9992        .await
9993        .unwrap();
9994        assert_eq!(signed_count, 0);
9995        assert!(
9996            state
9997                .recorded_requests()
9998                .iter()
9999                .all(|request| { request["method"] != "eth_sendRawTransaction" })
10000        );
10001        assert!(matches!(
10002            *client.in_flight.lock(),
10003            Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
10004        ));
10005
10006        drop_execution_schema(&admin_pool, &schema).await;
10007    }
10008
10009    #[tokio::test]
10010    async fn finalized_swap_emits_exact_fill_once_and_refreshes_wallet() {
10011        let min_amount_out = expected_min_amount_out(50);
10012        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10013        let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
10014        let Some((admin_pool, schema, mut client, state, _)) =
10015            swap_client_with_database("execution_submit_fill_test", state).await
10016        else {
10017            return;
10018        };
10019        let order = test_market_sell_order(test_pool().instrument_id);
10020        let mut receiver = start_with_events(&mut client);
10021
10022        let plan = client
10023            .prepare_swap(&submit_order_cmd(&order), &order)
10024            .unwrap();
10025        execute_swap(
10026            plan,
10027            client.transaction_executor().unwrap(),
10028            client.emitter.clone(),
10029            client.transaction_limits.max_quote_age_blocks,
10030            client.transaction_limits.deadline_seconds,
10031        )
10032        .await
10033        .unwrap();
10034
10035        let (nonce, wallet_address, transaction_to, transaction_input, transaction_value): (
10036            i64,
10037            String,
10038            String,
10039            String,
10040            String,
10041        ) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10042            "SELECT nonce, wallet_address, transaction_to, transaction_input, transaction_value \
10043             FROM {schema}.execution_intent"
10044        )))
10045        .fetch_one(&admin_pool)
10046        .await
10047        .unwrap();
10048        let finalized_block = client
10049            .http_rpc_client
10050            .block_by_number(FIXTURE_BLOCK + 1, true)
10051            .await
10052            .unwrap();
10053        let finalized_transaction = finalized_block
10054            .transactions
10055            .iter()
10056            .find(|transaction| transaction.hash == expected_hash)
10057            .unwrap();
10058        assert_eq!(finalized_transaction.from.to_string(), wallet_address);
10059        assert_eq!(finalized_transaction.nonce, u64::try_from(nonce).unwrap());
10060        assert_eq!(
10061            finalized_transaction.to,
10062            Some(Address::from_str(&transaction_to).unwrap())
10063        );
10064        assert_eq!(
10065            finalized_transaction.input.as_ref(),
10066            hex::decode(transaction_input.strip_prefix("0x").unwrap()).unwrap()
10067        );
10068        assert_eq!(
10069            finalized_transaction.value,
10070            U256::from_str(&transaction_value).unwrap()
10071        );
10072
10073        let mut order_events = Vec::new();
10074        let mut account_states = Vec::new();
10075
10076        while let Ok(event) = receiver.try_recv() {
10077            match event {
10078                ExecutionEvent::Order(event) => order_events.push(event),
10079                ExecutionEvent::Account(state) => account_states.push(state),
10080                other => panic!("unexpected execution event: {other:?}"),
10081            }
10082        }
10083        assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10084        assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10085        let OrderEventAny::Filled(fill) = &order_events[1] else {
10086            panic!("expected OrderFilled, was {:?}", order_events[1]);
10087        };
10088        let expected_commission = Money::from_u256(
10089            U256::from(50_112_u64) * U256::from(100_000_000_u64),
10090            test_pool().chain.native_currency(),
10091        )
10092        .unwrap();
10093        assert_eq!(fill.client_order_id, order.client_order_id());
10094        assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10095        assert_eq!(fill.order_side, OrderSide::Sell);
10096        assert_eq!(fill.last_qty, Quantity::from("0.001"));
10097        assert_eq!(fill.last_px, Price::from("1000"));
10098        assert_eq!(fill.currency.code.as_str(), "USDC");
10099        assert_eq!(fill.commission, Some(expected_commission));
10100        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10101        assert_eq!(account_states.len(), 1);
10102        let account_state = &account_states[0];
10103        assert_eq!(account_state.account_id, AccountId::from("BLOCKCHAIN-001"));
10104        assert_eq!(account_state.account_type, AccountType::Wallet);
10105        assert_eq!(account_state.base_currency, None);
10106        assert_eq!(account_state.balances.len(), 3);
10107        assert!(account_state.margins.is_empty());
10108        assert!(account_state.is_reported);
10109        assert_eq!(
10110            account_state.balances,
10111            client.wallet_balance.lock().as_account_balances().unwrap()
10112        );
10113
10114        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10115            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10116        )))
10117        .fetch_one(&admin_pool)
10118        .await
10119        .unwrap();
10120        assert!(fill_emitted);
10121        assert!(!active);
10122
10123        let database = client.cache.database.as_ref().unwrap().clone();
10124        let payload_keys = client.payload_keys.clone();
10125        let restart_config = client.config.clone();
10126        drop(client);
10127        let (mut restarted, _) = swap_client_with_cache(restart_config);
10128        restarted.cache.database = Some(database);
10129        restarted.payload_keys = payload_keys;
10130        restarted.signer = Some(Arc::new(
10131            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10132        ));
10133        let mut restart_receiver = start_with_events(&mut restarted);
10134        restarted.reconcile_unresolved_execution().await.unwrap();
10135        restarted.reconcile_unresolved_execution().await.unwrap();
10136        assert!(collect_order_events(&mut restart_receiver).is_empty());
10137        let requests = state.recorded_requests();
10138        assert_eq!(
10139            requests
10140                .iter()
10141                .filter(|request| request["method"] == "eth_sendRawTransaction")
10142                .count(),
10143            1
10144        );
10145        assert_eq!(
10146            requests
10147                .iter()
10148                .filter(|request| {
10149                    request["method"] == "eth_call"
10150                        && request["params"][0]["data"]
10151                            .as_str()
10152                            .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
10153                })
10154                .count(),
10155            9
10156        );
10157        assert_eq!(
10158            requests
10159                .iter()
10160                .filter(|request| request["method"] == "eth_getBalance")
10161                .count(),
10162            6
10163        );
10164
10165        drop_execution_schema(&admin_pool, &schema).await;
10166    }
10167
10168    #[tokio::test]
10169    async fn restart_emits_committed_swap_from_verified_inclusion_header() {
10170        let min_amount_out = expected_min_amount_out(50);
10171        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10172        let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
10173        let Some((admin_pool, schema, mut client, _, _)) =
10174            swap_client_with_database("execution_committed_fill_restart_test", state).await
10175        else {
10176            return;
10177        };
10178        let order = test_market_sell_order(test_pool().instrument_id);
10179        let mut receiver = start_with_events(&mut client);
10180        let plan = client
10181            .prepare_swap(&submit_order_cmd(&order), &order)
10182            .unwrap();
10183
10184        execute_swap(
10185            plan,
10186            client.transaction_executor().unwrap(),
10187            client.emitter.clone(),
10188            client.transaction_limits.max_quote_age_blocks,
10189            client.transaction_limits.deadline_seconds,
10190        )
10191        .await
10192        .unwrap();
10193        assert_swap_submitted_and_filled(&collect_order_events(&mut receiver));
10194        let ancestry_range: (i64, i64) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10195            "SELECT height_start, height_end FROM {schema}.execution_verification_decision \
10196             WHERE decision_class = 'finality' AND read_class = 'numbered_block' \
10197               AND height_end > height_start"
10198        )))
10199        .fetch_one(&admin_pool)
10200        .await
10201        .unwrap();
10202        assert_eq!(
10203            ancestry_range,
10204            ((FIXTURE_BLOCK + 1) as i64, (FIXTURE_BLOCK + 2) as i64)
10205        );
10206
10207        sqlx::query(sqlx::AssertSqlSafe(format!(
10208            "UPDATE {schema}.execution_intent SET fill_emitted = FALSE, active = TRUE"
10209        )))
10210        .execute(&admin_pool)
10211        .await
10212        .unwrap();
10213        let database = client.cache.database.as_ref().unwrap().clone();
10214        let payload_keys = client.payload_keys.clone();
10215        let restart_config = client.config.clone();
10216        drop(client);
10217        let (mut restarted, _) = swap_client_with_cache(restart_config);
10218        restarted.cache.database = Some(database);
10219        restarted.payload_keys = payload_keys;
10220        restarted.signer = Some(Arc::new(
10221            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10222        ));
10223        let mut restart_receiver = start_with_events(&mut restarted);
10224
10225        restarted.reconcile_unresolved_execution().await.unwrap();
10226
10227        let events = collect_order_events(&mut restart_receiver);
10228        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10229            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10230        )))
10231        .fetch_one(&admin_pool)
10232        .await
10233        .unwrap();
10234        assert_eq!(events.len(), 1, "was: {events:?}");
10235        assert!(matches!(&events[0], OrderEventAny::Filled(_)));
10236        assert!(fill_emitted);
10237        assert!(!active);
10238
10239        drop_execution_schema(&admin_pool, &schema).await;
10240    }
10241
10242    #[tokio::test]
10243    async fn finalized_swap_ignores_unrelated_swap_logs() {
10244        let min_amount_out = expected_min_amount_out(50);
10245        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10246        let receipt = finalized_swap_receipt_with_unrelated_swap(expected_hash);
10247        let state = finalized_swap_rpc_state(expected_hash, min_amount_out)
10248            .with_response("eth_getTransactionReceipt", &receipt);
10249        let Some((admin_pool, schema, mut client, _, _)) =
10250            swap_client_with_database("execution_submit_unrelated_log_test", state).await
10251        else {
10252            return;
10253        };
10254        let order = test_market_sell_order(test_pool().instrument_id);
10255        let mut receiver = start_with_events(&mut client);
10256
10257        client.submit_order(submit_order_cmd(&order)).unwrap();
10258        await_pending_tasks(&client).await;
10259
10260        let events = collect_order_events(&mut receiver);
10261        assert_swap_submitted_and_filled(&events);
10262
10263        drop_execution_schema(&admin_pool, &schema).await;
10264    }
10265
10266    #[tokio::test]
10267    async fn prepare_swap_buy_accepts_quote_spend_exact_boundary() {
10268        let amount_in = expected_buy_amount_in();
10269        let min_amount_out = expected_buy_min_amount_out(50);
10270        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10271        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10272        let max_amount = amount_in.to_string();
10273        let Some((admin_pool, schema, client, _, cache)) = swap_client_with_database_config(
10274            "execution_prepare_buy_test",
10275            state,
10276            move |http_rpc_url| {
10277                let mut config = buy_test_config(http_rpc_url);
10278                config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount;
10279                config
10280            },
10281        )
10282        .await
10283        else {
10284            return;
10285        };
10286        let order = test_market_buy_order(test_pool().instrument_id);
10287        cache
10288            .borrow_mut()
10289            .add_order(order.clone(), None, None, true)
10290            .unwrap();
10291
10292        let plan = client
10293            .prepare_swap(&submit_order_cmd(&order), &order)
10294            .unwrap();
10295
10296        assert_eq!(plan.token_in, USDC_ADDRESS);
10297        assert_eq!(plan.token_out, WETH_ADDRESS);
10298        assert_eq!(plan.amount_in, amount_in);
10299        assert_eq!(plan.min_amount_out, min_amount_out);
10300        assert_ne!(plan.token_in, WETH_ADDRESS);
10301
10302        drop_execution_schema(&admin_pool, &schema).await;
10303    }
10304
10305    #[tokio::test]
10306    async fn submit_order_broadcasts_buy_swap() {
10307        let amount_in = expected_buy_amount_in();
10308        let min_amount_out = expected_buy_min_amount_out(50);
10309        let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10310        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10311        let Some((admin_pool, schema, mut client, state, cache)) =
10312            swap_client_with_buy_database("execution_submit_buy_success_test", state).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        let mut receiver = start_with_events(&mut client);
10322
10323        client.submit_order(submit_order_cmd(&order)).unwrap();
10324        await_pending_tasks(&client).await;
10325
10326        let events = collect_order_events(&mut receiver);
10327        assert_swap_submitted_and_filled(&events);
10328        let broadcasts: Vec<_> = state
10329            .recorded_requests()
10330            .into_iter()
10331            .filter(|request| request["method"] == "eth_sendRawTransaction")
10332            .collect();
10333        assert_eq!(broadcasts.len(), 1);
10334        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10335
10336        drop_execution_schema(&admin_pool, &schema).await;
10337    }
10338
10339    #[tokio::test]
10340    async fn finalized_buy_swap_emits_fill_from_output_leg() {
10341        let amount_in = expected_buy_amount_in();
10342        let min_amount_out = expected_buy_min_amount_out(50);
10343        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10344        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10345        let Some((admin_pool, schema, mut client, state, cache)) =
10346            swap_client_with_buy_database("execution_submit_buy_fill_test", state).await
10347        else {
10348            return;
10349        };
10350        let order = test_market_buy_order(test_pool().instrument_id);
10351        cache
10352            .borrow_mut()
10353            .add_order(order.clone(), None, None, true)
10354            .unwrap();
10355        let mut receiver = start_with_events(&mut client);
10356        let plan = client
10357            .prepare_swap(&submit_order_cmd(&order), &order)
10358            .unwrap();
10359
10360        execute_swap(
10361            plan,
10362            client.transaction_executor().unwrap(),
10363            client.emitter.clone(),
10364            client.transaction_limits.max_quote_age_blocks,
10365            client.transaction_limits.deadline_seconds,
10366        )
10367        .await
10368        .unwrap();
10369
10370        let mut order_events = Vec::new();
10371
10372        while let Ok(event) = receiver.try_recv() {
10373            if let ExecutionEvent::Order(event) = event {
10374                order_events.push(event);
10375            }
10376        }
10377        assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10378        assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10379        let OrderEventAny::Filled(fill) = &order_events[1] else {
10380            panic!("expected OrderFilled, was {:?}", order_events[1]);
10381        };
10382        let expected_commission = Money::from_u256(
10383            U256::from(50_112_u64) * U256::from(100_000_000_u64),
10384            test_pool().chain.native_currency(),
10385        )
10386        .unwrap();
10387        assert_eq!(fill.client_order_id, order.client_order_id());
10388        assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10389        assert_eq!(fill.order_side, OrderSide::Buy);
10390        assert_eq!(fill.last_qty, Quantity::from("0.001"));
10391        assert_eq!(fill.currency.code.as_str(), "USDC");
10392        assert_eq!(fill.commission, Some(expected_commission));
10393        assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10394        assert_eq!(
10395            Money::from_decimal(
10396                fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10397                fill.currency,
10398            )
10399            .unwrap(),
10400            Money::from_u256(amount_in, fill.currency).unwrap()
10401        );
10402
10403        let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10404            "SELECT fill_emitted, active FROM {schema}.execution_intent"
10405        )))
10406        .fetch_one(&admin_pool)
10407        .await
10408        .unwrap();
10409        assert!(fill_emitted);
10410        assert!(!active);
10411
10412        let database = client.cache.database.as_ref().unwrap().clone();
10413        let payload_keys = client.payload_keys.clone();
10414        let restart_config = client.config.clone();
10415        drop(client);
10416        let (mut restarted, _) = swap_client_with_cache(restart_config);
10417        restarted.cache.database = Some(database);
10418        restarted.payload_keys = payload_keys;
10419        restarted.signer = Some(Arc::new(
10420            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10421        ));
10422        let mut restart_receiver = start_with_events(&mut restarted);
10423        restarted.reconcile_unresolved_execution().await.unwrap();
10424        restarted.reconcile_unresolved_execution().await.unwrap();
10425        assert!(collect_order_events(&mut restart_receiver).is_empty());
10426        assert_eq!(
10427            state
10428                .recorded_requests()
10429                .iter()
10430                .filter(|request| request["method"] == "eth_sendRawTransaction")
10431                .count(),
10432            1
10433        );
10434
10435        drop_execution_schema(&admin_pool, &schema).await;
10436    }
10437
10438    #[tokio::test]
10439    async fn finalized_buy_swap_cancels_remainder_when_output_is_short() {
10440        let amount_in = expected_buy_amount_in();
10441        let min_amount_out = expected_buy_min_amount_out(50);
10442        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10443        let short_base_out = min_amount_out;
10444        let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10445        state = state.with_response(
10446            "eth_getTransactionReceipt",
10447            &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, short_base_out),
10448        );
10449        let Some((admin_pool, schema, mut client, _, cache)) =
10450            swap_client_with_buy_database("execution_submit_buy_short_fill_test", state).await
10451        else {
10452            return;
10453        };
10454        let order = test_market_buy_order(test_pool().instrument_id);
10455        cache
10456            .borrow_mut()
10457            .add_order(order.clone(), None, None, true)
10458            .unwrap();
10459        let mut receiver = start_with_events(&mut client);
10460        let plan = client
10461            .prepare_swap(&submit_order_cmd(&order), &order)
10462            .unwrap();
10463
10464        execute_swap(
10465            plan,
10466            client.transaction_executor().unwrap(),
10467            client.emitter.clone(),
10468            client.transaction_limits.max_quote_age_blocks,
10469            client.transaction_limits.deadline_seconds,
10470        )
10471        .await
10472        .unwrap();
10473
10474        let events = collect_order_events(&mut receiver);
10475        assert_eq!(events.len(), 3, "was: {events:?}");
10476        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10477        let OrderEventAny::Filled(fill) = &events[1] else {
10478            panic!("expected OrderFilled, was {:?}", events[1]);
10479        };
10480        let expected_qty = raw_amount_to_quantity(short_base_out, 18).unwrap();
10481        assert_eq!(fill.order_side, OrderSide::Buy);
10482        assert_eq!(fill.last_qty, expected_qty);
10483        assert!(fill.last_qty < order.quantity());
10484        let OrderEventAny::Canceled(canceled) = &events[2] else {
10485            panic!("expected OrderCanceled, was {:?}", events[2]);
10486        };
10487        assert_eq!(canceled.client_order_id, order.client_order_id());
10488        assert_eq!(
10489            canceled.venue_order_id.as_ref().map(VenueOrderId::as_str),
10490            Some(expected_hash.to_string().as_str())
10491        );
10492
10493        drop_execution_schema(&admin_pool, &schema).await;
10494    }
10495
10496    #[tokio::test]
10497    async fn finalized_buy_swap_reports_full_output_when_above_order_quantity() {
10498        let amount_in = expected_buy_amount_in();
10499        let min_amount_out = expected_buy_min_amount_out(50);
10500        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10501        let overshoot_base_out = expected_buy_base_amount() * U256::from(2) + U256::from(44);
10502        let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10503        state = state.with_response(
10504            "eth_getTransactionReceipt",
10505            &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, overshoot_base_out),
10506        );
10507        let Some((admin_pool, schema, mut client, _, cache)) =
10508            swap_client_with_buy_database("execution_submit_buy_overshoot_fill_test", state).await
10509        else {
10510            return;
10511        };
10512        let order = test_market_buy_order(test_pool().instrument_id);
10513        cache
10514            .borrow_mut()
10515            .add_order(order.clone(), None, None, true)
10516            .unwrap();
10517        let mut receiver = start_with_events(&mut client);
10518        let plan = client
10519            .prepare_swap(&submit_order_cmd(&order), &order)
10520            .unwrap();
10521
10522        execute_swap(
10523            plan,
10524            client.transaction_executor().unwrap(),
10525            client.emitter.clone(),
10526            client.transaction_limits.max_quote_age_blocks,
10527            client.transaction_limits.deadline_seconds,
10528        )
10529        .await
10530        .unwrap();
10531
10532        let events = collect_order_events(&mut receiver);
10533        assert_eq!(events.len(), 2, "was: {events:?}");
10534        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10535        let OrderEventAny::Filled(fill) = &events[1] else {
10536            panic!("expected OrderFilled, was {:?}", events[1]);
10537        };
10538        let expected_qty = raw_amount_to_quantity(overshoot_base_out, 18).unwrap();
10539        assert_eq!(fill.order_side, OrderSide::Buy);
10540        assert_eq!(fill.last_qty, expected_qty);
10541        assert!(fill.last_qty > order.quantity());
10542        assert_eq!(
10543            Money::from_decimal(
10544                fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10545                fill.currency,
10546            )
10547            .unwrap(),
10548            Money::from_u256(amount_in, fill.currency).unwrap()
10549        );
10550
10551        drop_execution_schema(&admin_pool, &schema).await;
10552    }
10553
10554    #[tokio::test]
10555    async fn finalized_buy_swap_quarantines_sell_oriented_log() {
10556        let amount_in = expected_buy_amount_in();
10557        let min_amount_out = expected_buy_min_amount_out(50);
10558        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10559        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10560            .with_response(
10561                "eth_getTransactionReceipt",
10562                &finalized_swap_receipt(expected_hash),
10563            );
10564        let Some((admin_pool, schema, mut client, _, cache)) =
10565            swap_client_with_buy_database("execution_submit_buy_sell_log_test", state).await
10566        else {
10567            return;
10568        };
10569        let order = test_market_buy_order(test_pool().instrument_id);
10570        cache
10571            .borrow_mut()
10572            .add_order(order.clone(), None, None, true)
10573            .unwrap();
10574        let mut receiver = start_with_events(&mut client);
10575        let plan = client
10576            .prepare_swap(&submit_order_cmd(&order), &order)
10577            .unwrap();
10578
10579        let error = execute_swap(
10580            plan,
10581            client.transaction_executor().unwrap(),
10582            client.emitter.clone(),
10583            client.transaction_limits.max_quote_age_blocks,
10584            client.transaction_limits.deadline_seconds,
10585        )
10586        .await
10587        .unwrap_err();
10588
10589        let events = collect_order_events(&mut receiver);
10590        assert_swap_quarantined_without_terminal_event(&events);
10591        assert!(
10592            error
10593                .to_string()
10594                .contains("does not match the persisted amount")
10595                || error.to_string().contains("is not a BUY output"),
10596            "was: {error}"
10597        );
10598        assert!(client.in_flight.lock().is_some());
10599
10600        drop_execution_schema(&admin_pool, &schema).await;
10601    }
10602
10603    #[tokio::test]
10604    async fn submit_order_applies_buy_slippage_to_base_output() {
10605        let amount_in = expected_buy_amount_in();
10606        let min_amount_out = expected_buy_min_amount_out(200);
10607        let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10608        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10609        let Some((admin_pool, schema, mut client, state, cache)) =
10610            swap_client_with_buy_database("execution_submit_buy_slippage_test", state).await
10611        else {
10612            return;
10613        };
10614        let order = test_market_buy_order(test_pool().instrument_id);
10615        cache
10616            .borrow_mut()
10617            .add_order(order.clone(), None, None, true)
10618            .unwrap();
10619        let mut cmd = submit_order_cmd(&order);
10620        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
10621        let mut receiver = start_with_events(&mut client);
10622
10623        client.submit_order(cmd).unwrap();
10624        await_pending_tasks(&client).await;
10625
10626        let events = collect_order_events(&mut receiver);
10627        assert_swap_submitted_and_filled(&events);
10628        let broadcasts: Vec<_> = state
10629            .recorded_requests()
10630            .into_iter()
10631            .filter(|request| request["method"] == "eth_sendRawTransaction")
10632            .collect();
10633        assert_eq!(broadcasts.len(), 1);
10634        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10635
10636        drop_execution_schema(&admin_pool, &schema).await;
10637    }
10638
10639    #[tokio::test]
10640    async fn submit_order_denies_buy_on_insufficient_quote_balance() {
10641        let amount_in = expected_buy_amount_in();
10642        let min_amount_out = expected_buy_min_amount_out(50);
10643        let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10644        let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10645            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10646        let Some((admin_pool, schema, mut client, _, cache)) =
10647            swap_client_with_buy_database("execution_submit_buy_balance_test", state).await
10648        else {
10649            return;
10650        };
10651        let order = test_market_buy_order(test_pool().instrument_id);
10652        cache
10653            .borrow_mut()
10654            .add_order(order.clone(), None, None, true)
10655            .unwrap();
10656        let mut receiver = start_with_events(&mut client);
10657
10658        client.submit_order(submit_order_cmd(&order)).unwrap();
10659        await_pending_tasks(&client).await;
10660
10661        let events = collect_order_events(&mut receiver);
10662        assert_eq!(events.len(), 1);
10663        let OrderEventAny::Denied(denied) = &events[0] else {
10664            panic!("expected OrderDenied, was {:?}", events[0]);
10665        };
10666        assert!(
10667            denied.reason.as_str().contains("is below the swap amount"),
10668            "was: {}",
10669            denied.reason
10670        );
10671
10672        drop_execution_schema(&admin_pool, &schema).await;
10673    }
10674
10675    #[tokio::test]
10676    async fn finalized_swap_without_log_stays_quarantined() {
10677        let state = swap_rpc_state()
10678            .await
10679            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS);
10680        let Some((admin_pool, schema, mut client, _, _)) =
10681            swap_client_with_database("execution_submit_missing_log_test", state).await
10682        else {
10683            return;
10684        };
10685        let order = test_market_sell_order(test_pool().instrument_id);
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        let (status, terminal_emitted, active): (String, bool, bool) =
10693            sqlx::query_as(sqlx::AssertSqlSafe(format!(
10694                "SELECT status, terminal_emitted, active FROM {schema}.execution_intent"
10695            )))
10696            .fetch_one(&admin_pool)
10697            .await
10698            .unwrap();
10699
10700        assert_swap_quarantined_without_terminal_event(&events);
10701        assert_eq!(status, "broadcast");
10702        assert!(!terminal_emitted);
10703        assert!(active);
10704        assert!(client.in_flight.lock().is_some());
10705
10706        drop_execution_schema(&admin_pool, &schema).await;
10707    }
10708
10709    #[tokio::test]
10710    async fn finalized_swap_refresh_failure_stays_owned_for_reconciliation() {
10711        let min_amount_out = expected_min_amount_out(50);
10712        let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10713        let state = finalized_swap_rpc_state(expected_hash, min_amount_out).with_response_sequence(
10714            "eth_getBalance",
10715            &[
10716                GET_BALANCE,
10717                GET_BALANCE,
10718                GET_BALANCE,
10719                RPC_METHOD_NOT_FOUND,
10720                RPC_METHOD_NOT_FOUND,
10721                RPC_METHOD_NOT_FOUND,
10722            ],
10723        );
10724        let Some((admin_pool, schema, mut client, _state, _)) =
10725            swap_client_with_database("execution_submit_refresh_fail_test", state).await
10726        else {
10727            return;
10728        };
10729        let order = test_market_sell_order(test_pool().instrument_id);
10730        let mut receiver = start_with_events(&mut client);
10731        let plan = client
10732            .prepare_swap(&submit_order_cmd(&order), &order)
10733            .unwrap();
10734
10735        let error = execute_swap(
10736            plan,
10737            client.transaction_executor().unwrap(),
10738            client.emitter.clone(),
10739            client.transaction_limits.max_quote_age_blocks,
10740            client.transaction_limits.deadline_seconds,
10741        )
10742        .await
10743        .unwrap_err();
10744        let events = collect_order_events(&mut receiver);
10745        let (status, fill_emitted, active): (String, bool, bool) =
10746            sqlx::query_as(sqlx::AssertSqlSafe(format!(
10747                "SELECT status, fill_emitted, active FROM {schema}.execution_intent"
10748            )))
10749            .fetch_one(&admin_pool)
10750            .await
10751            .unwrap();
10752
10753        assert!(
10754            error
10755                .to_string()
10756                .contains("finalized native balance verification is locally invalid"),
10757            "was: {error}"
10758        );
10759        assert_eq!(events.len(), 1, "was: {events:?}");
10760        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10761        assert_eq!(status, "broadcast");
10762        assert!(!fill_emitted);
10763        assert!(active);
10764        assert!(client.in_flight.lock().is_some());
10765
10766        drop_execution_schema(&admin_pool, &schema).await;
10767    }
10768
10769    #[tokio::test]
10770    async fn submit_order_denies_router_factory_mismatch_before_signing() {
10771        let state = swap_rpc_state()
10772            .await
10773            .with_call_response(FACTORY_SELECTOR, CALL_ZERO);
10774        let Some((admin_pool, schema, mut client, state, _)) =
10775            swap_client_with_database("execution_submit_factory_test", state).await
10776        else {
10777            return;
10778        };
10779        let order = test_market_sell_order(test_pool().instrument_id);
10780        let mut receiver = start_with_events(&mut client);
10781
10782        client.submit_order(submit_order_cmd(&order)).unwrap();
10783        await_pending_tasks(&client).await;
10784
10785        let events = collect_order_events(&mut receiver);
10786        assert_eq!(events.len(), 1);
10787        let OrderEventAny::Denied(denied) = &events[0] else {
10788            panic!("expected OrderDenied, was {:?}", events[0]);
10789        };
10790        assert!(
10791            denied
10792                .reason
10793                .as_str()
10794                .contains("swap deployment manifest verification disagreed"),
10795            "was: {}",
10796            denied.reason
10797        );
10798        let requests = state.recorded_requests();
10799        assert!(
10800            requests
10801                .iter()
10802                .all(|request| request["method"] != "eth_getTransactionCount")
10803        );
10804        assert!(
10805            requests
10806                .iter()
10807                .all(|request| request["method"] != "eth_sendRawTransaction")
10808        );
10809
10810        drop_execution_schema(&admin_pool, &schema).await;
10811    }
10812
10813    #[tokio::test]
10814    async fn submit_order_denies_router_weth_mismatch_before_signing() {
10815        let state = swap_rpc_state()
10816            .await
10817            .with_call_response(WETH9_SELECTOR, CALL_ZERO);
10818        let Some((admin_pool, schema, mut client, state, _)) =
10819            swap_client_with_database("execution_submit_weth_test", state).await
10820        else {
10821            return;
10822        };
10823        let order = test_market_sell_order(test_pool().instrument_id);
10824        let mut receiver = start_with_events(&mut client);
10825
10826        client.submit_order(submit_order_cmd(&order)).unwrap();
10827        await_pending_tasks(&client).await;
10828
10829        let events = collect_order_events(&mut receiver);
10830        assert_eq!(events.len(), 1);
10831        let OrderEventAny::Denied(denied) = &events[0] else {
10832            panic!("expected OrderDenied, was {:?}", events[0]);
10833        };
10834        assert!(
10835            denied
10836                .reason
10837                .as_str()
10838                .contains("swap deployment manifest verification disagreed"),
10839            "was: {}",
10840            denied.reason
10841        );
10842        let requests = state.recorded_requests();
10843        assert!(
10844            requests
10845                .iter()
10846                .all(|request| request["method"] != "eth_getTransactionCount")
10847        );
10848        assert!(
10849            requests
10850                .iter()
10851                .all(|request| request["method"] != "eth_sendRawTransaction")
10852        );
10853
10854        drop_execution_schema(&admin_pool, &schema).await;
10855    }
10856
10857    #[tokio::test]
10858    async fn submit_order_denies_factory_pool_mismatch_before_signing() {
10859        let state = swap_rpc_state()
10860            .await
10861            .with_call_response(GET_POOL_SELECTOR, CALL_ZERO);
10862        let Some((admin_pool, schema, mut client, state, _)) =
10863            swap_client_with_database("execution_submit_pool_identity_test", state).await
10864        else {
10865            return;
10866        };
10867        let order = test_market_sell_order(test_pool().instrument_id);
10868        let mut receiver = start_with_events(&mut client);
10869
10870        client.submit_order(submit_order_cmd(&order)).unwrap();
10871        await_pending_tasks(&client).await;
10872
10873        let events = collect_order_events(&mut receiver);
10874        assert_eq!(events.len(), 1);
10875        let OrderEventAny::Denied(denied) = &events[0] else {
10876            panic!("expected OrderDenied, was {:?}", events[0]);
10877        };
10878        assert!(
10879            denied
10880                .reason
10881                .as_str()
10882                .contains("swap deployment manifest verification disagreed"),
10883            "was: {}",
10884            denied.reason
10885        );
10886        let requests = state.recorded_requests();
10887        assert!(
10888            requests
10889                .iter()
10890                .all(|request| request["method"] != "eth_getTransactionCount")
10891        );
10892        assert!(
10893            requests
10894                .iter()
10895                .all(|request| request["method"] != "eth_sendRawTransaction")
10896        );
10897
10898        drop_execution_schema(&admin_pool, &schema).await;
10899    }
10900
10901    #[tokio::test]
10902    async fn submit_order_denies_cached_token_decimal_mismatch_before_signing() {
10903        let state = swap_rpc_state().await.with_contract_call_response(
10904            WETH,
10905            DECIMALS_SELECTOR,
10906            CALL_DECIMALS_6,
10907        );
10908        let Some((admin_pool, schema, mut client, state, _)) =
10909            swap_client_with_database("execution_submit_decimals_test", state).await
10910        else {
10911            return;
10912        };
10913        let order = test_market_sell_order(test_pool().instrument_id);
10914        let mut receiver = start_with_events(&mut client);
10915
10916        client.submit_order(submit_order_cmd(&order)).unwrap();
10917        await_pending_tasks(&client).await;
10918
10919        let events = collect_order_events(&mut receiver);
10920        assert_eq!(events.len(), 1);
10921        let OrderEventAny::Denied(denied) = &events[0] else {
10922            panic!("expected OrderDenied, was {:?}", events[0]);
10923        };
10924        assert!(
10925            denied
10926                .reason
10927                .as_str()
10928                .contains("swap deployment manifest verification disagreed"),
10929            "was: {}",
10930            denied.reason
10931        );
10932        let requests = state.recorded_requests();
10933        assert!(
10934            requests
10935                .iter()
10936                .all(|request| request["method"] != "eth_getTransactionCount")
10937        );
10938        assert!(
10939            requests
10940                .iter()
10941                .all(|request| request["method"] != "eth_sendRawTransaction")
10942        );
10943
10944        drop_execution_schema(&admin_pool, &schema).await;
10945    }
10946
10947    #[tokio::test]
10948    async fn submit_order_denies_on_insufficient_router_allowance() {
10949        let state = swap_rpc_state()
10950            .await
10951            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
10952        let Some((admin_pool, schema, mut client, state, _)) =
10953            swap_client_with_database("execution_submit_allowance_test", state).await
10954        else {
10955            return;
10956        };
10957        let order = test_market_sell_order(test_pool().instrument_id);
10958        let mut receiver = start_with_events(&mut client);
10959
10960        client.submit_order(submit_order_cmd(&order)).unwrap();
10961        await_pending_tasks(&client).await;
10962
10963        let events = collect_order_events(&mut receiver);
10964        assert_eq!(events.len(), 1);
10965        let OrderEventAny::Denied(denied) = &events[0] else {
10966            panic!("expected OrderDenied, was {:?}", events[0]);
10967        };
10968        assert!(
10969            denied.reason.as_str().contains("below the swap amount"),
10970            "was: {}",
10971            denied.reason
10972        );
10973        let requests = state.recorded_requests();
10974        assert!(
10975            requests
10976                .iter()
10977                .all(|request| request["method"] != "eth_sendRawTransaction"),
10978            "no broadcast may follow a pre-trade denial"
10979        );
10980        let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
10981            "SELECT COUNT(*) FROM {schema}.execution_intent"
10982        )))
10983        .fetch_one(&admin_pool)
10984        .await
10985        .unwrap();
10986        assert_eq!(row_count, 0);
10987        assert!(client.in_flight.lock().is_none());
10988
10989        drop_execution_schema(&admin_pool, &schema).await;
10990    }
10991
10992    #[tokio::test]
10993    async fn submit_order_denies_on_insufficient_input_balance() {
10994        let state = swap_rpc_state()
10995            .await
10996            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10997        let Some((admin_pool, schema, mut client, _, _)) =
10998            swap_client_with_database("execution_submit_balance_test", state).await
10999        else {
11000            return;
11001        };
11002        let order = test_market_sell_order(test_pool().instrument_id);
11003        let mut receiver = start_with_events(&mut client);
11004
11005        client.submit_order(submit_order_cmd(&order)).unwrap();
11006        await_pending_tasks(&client).await;
11007
11008        let events = collect_order_events(&mut receiver);
11009        assert_eq!(events.len(), 1);
11010        let OrderEventAny::Denied(denied) = &events[0] else {
11011            panic!("expected OrderDenied, was {:?}", events[0]);
11012        };
11013        assert!(
11014            denied.reason.as_str().contains("is below the swap amount"),
11015            "was: {}",
11016            denied.reason
11017        );
11018
11019        drop_execution_schema(&admin_pool, &schema).await;
11020    }
11021
11022    #[tokio::test]
11023    async fn submit_order_denies_on_insufficient_native_balance() {
11024        let state = swap_rpc_state()
11025            .await
11026            .with_response("eth_getBalance", GET_BALANCE_INSUFFICIENT);
11027        let Some((admin_pool, schema, mut client, state, _)) =
11028            swap_client_with_database("execution_submit_native_balance_test", state).await
11029        else {
11030            return;
11031        };
11032        let order = test_market_sell_order(test_pool().instrument_id);
11033        let mut receiver = start_with_events(&mut client);
11034
11035        client.submit_order(submit_order_cmd(&order)).unwrap();
11036        await_pending_tasks(&client).await;
11037
11038        let events = collect_order_events(&mut receiver);
11039        assert_eq!(events.len(), 1);
11040        let OrderEventAny::Denied(denied) = &events[0] else {
11041            panic!("expected OrderDenied, was {:?}", events[0]);
11042        };
11043        assert!(
11044            denied
11045                .reason
11046                .as_str()
11047                .contains("below maximum transaction cost"),
11048            "was: {}",
11049            denied.reason
11050        );
11051        assert!(
11052            state
11053                .recorded_requests()
11054                .iter()
11055                .all(|request| request["method"] != "eth_sendRawTransaction"),
11056            "no broadcast may follow an insufficient native balance"
11057        );
11058        let (row_count, status): (i64, Option<String>) = sqlx::query_as(sqlx::AssertSqlSafe(
11059            format!("SELECT COUNT(*), MAX(status) FROM {schema}.execution_intent"),
11060        ))
11061        .fetch_one(&admin_pool)
11062        .await
11063        .unwrap();
11064        assert_eq!(row_count, 1);
11065        assert_eq!(status.as_deref(), Some("recoverable"));
11066        assert!(client.in_flight.lock().is_none());
11067
11068        drop_execution_schema(&admin_pool, &schema).await;
11069    }
11070
11071    #[tokio::test]
11072    async fn submit_order_denies_on_stale_quote() {
11073        let Some((admin_pool, schema, mut client, state, cache)) =
11074            swap_client_with_database("execution_submit_stale_quote_test", swap_rpc_state().await)
11075                .await
11076        else {
11077            return;
11078        };
11079        let pool = test_pool();
11080        cache
11081            .borrow_mut()
11082            .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK - 101))
11083            .unwrap();
11084        let order = test_market_sell_order(pool.instrument_id);
11085        let mut receiver = start_with_events(&mut client);
11086
11087        client.submit_order(submit_order_cmd(&order)).unwrap();
11088        await_pending_tasks(&client).await;
11089
11090        let events = collect_order_events(&mut receiver);
11091        assert_eq!(events.len(), 1);
11092        let OrderEventAny::Denied(denied) = &events[0] else {
11093            panic!("expected OrderDenied, was {:?}", events[0]);
11094        };
11095        assert!(
11096            denied.reason.as_str().contains("Stale quote"),
11097            "was: {}",
11098            denied.reason
11099        );
11100        let requests = state.recorded_requests();
11101        assert!(
11102            requests
11103                .iter()
11104                .all(|request| request["method"] != "eth_sendRawTransaction"),
11105            "no broadcast may follow a pre-trade denial"
11106        );
11107
11108        drop_execution_schema(&admin_pool, &schema).await;
11109    }
11110
11111    #[tokio::test]
11112    async fn submit_order_denies_quote_ahead_of_chain_head() {
11113        let Some((admin_pool, schema, mut client, state, cache)) =
11114            swap_client_with_database("execution_submit_ahead_quote_test", swap_rpc_state().await)
11115                .await
11116        else {
11117            return;
11118        };
11119        let pool = test_pool();
11120        cache
11121            .borrow_mut()
11122            .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK + 1))
11123            .unwrap();
11124        let order = test_market_sell_order(pool.instrument_id);
11125        let mut receiver = start_with_events(&mut client);
11126
11127        client.submit_order(submit_order_cmd(&order)).unwrap();
11128        await_pending_tasks(&client).await;
11129
11130        let events = collect_order_events(&mut receiver);
11131        assert_eq!(events.len(), 1);
11132        let OrderEventAny::Denied(denied) = &events[0] else {
11133            panic!("expected OrderDenied, was {:?}", events[0]);
11134        };
11135        assert!(
11136            denied
11137                .reason
11138                .as_str()
11139                .contains("is ahead of the latest block"),
11140            "was: {}",
11141            denied.reason
11142        );
11143        let requests = state.recorded_requests();
11144        assert!(
11145            requests
11146                .iter()
11147                .all(|request| request["method"] != "eth_sendRawTransaction"),
11148            "no broadcast may follow a pre-trade denial"
11149        );
11150
11151        drop_execution_schema(&admin_pool, &schema).await;
11152    }
11153
11154    #[tokio::test]
11155    async fn submit_order_denies_on_deadline_overflow() {
11156        let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
11157            "execution_submit_deadline_overflow_test",
11158            swap_rpc_state().await,
11159        )
11160        .await
11161        else {
11162            return;
11163        };
11164        client.transaction_limits.deadline_seconds = u64::MAX;
11165        let order = test_market_sell_order(test_pool().instrument_id);
11166        let mut receiver = start_with_events(&mut client);
11167
11168        client.submit_order(submit_order_cmd(&order)).unwrap();
11169        await_pending_tasks(&client).await;
11170
11171        let events = collect_order_events(&mut receiver);
11172        assert_eq!(events.len(), 1);
11173        let OrderEventAny::Denied(denied) = &events[0] else {
11174            panic!("expected OrderDenied, was {:?}", events[0]);
11175        };
11176        assert!(
11177            denied.reason.as_str().contains("deadline overflow"),
11178            "was: {}",
11179            denied.reason
11180        );
11181        let requests = state.recorded_requests();
11182        assert!(
11183            requests
11184                .iter()
11185                .all(|request| request["method"] != "eth_sendRawTransaction"),
11186            "no broadcast may follow a pre-trade denial"
11187        );
11188        assert!(client.in_flight.lock().is_none());
11189
11190        drop_execution_schema(&admin_pool, &schema).await;
11191    }
11192
11193    #[tokio::test]
11194    async fn submit_order_reconciles_node_rejection_to_finalized_receipt() {
11195        let state = swap_rpc_state()
11196            .await
11197            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED);
11198        let Some((admin_pool, schema, mut client, _state, _)) =
11199            swap_client_with_database("execution_submit_node_rejected_test", state).await
11200        else {
11201            return;
11202        };
11203        let order = test_market_sell_order(test_pool().instrument_id);
11204        let mut receiver = start_with_events(&mut client);
11205
11206        client.submit_order(submit_order_cmd(&order)).unwrap();
11207        await_pending_tasks(&client).await;
11208
11209        let events = collect_order_events(&mut receiver);
11210        assert_swap_submitted_and_filled(&events);
11211        let (purpose, status, client_order_id): (String, String, Option<String>) =
11212            sqlx::query_as(sqlx::AssertSqlSafe(format!(
11213                "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11214            )))
11215            .fetch_one(&admin_pool)
11216            .await
11217            .unwrap();
11218        assert_eq!(purpose, "swap");
11219        assert_eq!(status, "finalized");
11220        assert_eq!(
11221            client_order_id.as_deref(),
11222            Some(order.client_order_id().as_str())
11223        );
11224        assert!(client.in_flight.lock().is_none());
11225
11226        drop_execution_schema(&admin_pool, &schema).await;
11227    }
11228
11229    #[tokio::test]
11230    async fn submit_order_acknowledges_uncertain_nonce_too_low() {
11231        let state = swap_rpc_state()
11232            .await
11233            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_NONCE_TOO_LOW);
11234        let Some((admin_pool, schema, mut client, _state, _)) =
11235            swap_client_with_database("execution_submit_nonce_too_low_test", state).await
11236        else {
11237            return;
11238        };
11239        let order = test_market_sell_order(test_pool().instrument_id);
11240        let mut receiver = start_with_events(&mut client);
11241
11242        client.submit_order(submit_order_cmd(&order)).unwrap();
11243        await_pending_tasks(&client).await;
11244
11245        let events = collect_order_events(&mut receiver);
11246        assert_swap_submitted_and_filled(&events);
11247        let (purpose, status, client_order_id): (String, String, Option<String>) =
11248            sqlx::query_as(sqlx::AssertSqlSafe(format!(
11249                "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11250            )))
11251            .fetch_one(&admin_pool)
11252            .await
11253            .unwrap();
11254        assert_eq!(purpose, "swap");
11255        assert_eq!(status, "finalized");
11256        assert_eq!(
11257            client_order_id.as_deref(),
11258            Some(order.client_order_id().as_str())
11259        );
11260        assert!(client.in_flight.lock().is_none());
11261
11262        drop_execution_schema(&admin_pool, &schema).await;
11263    }
11264
11265    #[tokio::test]
11266    async fn submit_order_rejected_on_reverted_receipt() {
11267        let expected_min_out = expected_min_amount_out(50);
11268        let (expected_hash, _) = expected_swap_tx(expected_min_out).await;
11269        let block = finalized_swap_block(expected_hash, expected_min_out);
11270        let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
11271        let state = with_finalized_identity(
11272            swap_rpc_state()
11273                .await
11274                .with_response("eth_getTransactionReceipt", &receipt),
11275            &block,
11276            &receipt,
11277        );
11278        let Some((admin_pool, schema, mut client, _state, _)) =
11279            swap_client_with_database("execution_submit_reverted_test", state).await
11280        else {
11281            return;
11282        };
11283        let order = test_market_sell_order(test_pool().instrument_id);
11284        let mut receiver = start_with_events(&mut client);
11285        client.submit_order(submit_order_cmd(&order)).unwrap();
11286        await_pending_tasks(&client).await;
11287
11288        let events = collect_order_events(&mut receiver);
11289        assert_eq!(events.len(), 2);
11290        assert!(
11291            matches!(&events[0], OrderEventAny::Submitted(_)),
11292            "was: {:?}",
11293            events[0]
11294        );
11295        let OrderEventAny::Rejected(rejected) = &events[1] else {
11296            panic!("expected OrderRejected, was {:?}", events[1]);
11297        };
11298        assert!(
11299            rejected.reason.as_str().contains("reverted on-chain"),
11300            "was: {}",
11301            rejected.reason
11302        );
11303
11304        let record = client
11305            .cache
11306            .get_execution_transaction(42161, &expected_hash.to_string())
11307            .await
11308            .unwrap()
11309            .unwrap();
11310        assert_eq!(record.purpose, "swap");
11311        assert_eq!(record.status, "reverted");
11312        assert_eq!(
11313            record.client_order_id.as_deref(),
11314            Some(order.client_order_id().as_str())
11315        );
11316        assert!(client.in_flight.lock().is_none());
11317
11318        drop_execution_schema(&admin_pool, &schema).await;
11319    }
11320
11321    #[tokio::test]
11322    async fn submit_order_acknowledges_ambiguous_broadcast() {
11323        // A response the client cannot parse classifies as an ambiguous broadcast failure:
11324        // the transaction may be live, so the durable intent remains submitted and occupied
11325        let state = swap_rpc_state()
11326            .await
11327            .with_response("eth_sendRawTransaction", "not json");
11328        let Some((admin_pool, schema, mut client, _state, _)) =
11329            swap_client_with_database("execution_submit_ambiguous_test", state).await
11330        else {
11331            return;
11332        };
11333        let order = test_market_sell_order(test_pool().instrument_id);
11334        let mut receiver = start_with_events(&mut client);
11335
11336        client.submit_order(submit_order_cmd(&order)).unwrap();
11337        await_pending_tasks(&client).await;
11338
11339        let events = collect_order_events(&mut receiver);
11340        assert_swap_submitted_and_filled(&events);
11341
11342        let record = client
11343            .cache
11344            .get_execution_transaction(
11345                42161,
11346                &expected_swap_tx(expected_min_amount_out(50))
11347                    .await
11348                    .0
11349                    .to_string(),
11350            )
11351            .await
11352            .unwrap()
11353            .unwrap();
11354        assert_eq!(record.purpose, "swap");
11355        assert_eq!(record.status, "finalized");
11356        assert!(client.in_flight.lock().is_none());
11357
11358        drop_execution_schema(&admin_pool, &schema).await;
11359    }
11360
11361    #[tokio::test]
11362    async fn submit_order_acknowledges_broadcast_hash_mismatch() {
11363        // The static fixture's hash differs from the locally signed hash, so acceptance of the
11364        // signed transaction is unverified until the independent receipt reaches finality
11365        let state = swap_rpc_state_for_mismatch().await;
11366        let Some((admin_pool, schema, mut client, _state, _)) =
11367            swap_client_with_database("execution_submit_hash_mismatch_test", state).await
11368        else {
11369            return;
11370        };
11371        let order = test_market_sell_order(test_pool().instrument_id);
11372        let mut receiver = start_with_events(&mut client);
11373
11374        client.submit_order(submit_order_cmd(&order)).unwrap();
11375        await_pending_tasks(&client).await;
11376
11377        let events = collect_order_events(&mut receiver);
11378        assert_swap_submitted_and_filled(&events);
11379
11380        let record = client
11381            .cache
11382            .get_execution_transaction(
11383                42161,
11384                &expected_swap_tx(expected_min_amount_out(50))
11385                    .await
11386                    .0
11387                    .to_string(),
11388            )
11389            .await
11390            .unwrap()
11391            .unwrap();
11392        assert_eq!(record.purpose, "swap");
11393        assert_eq!(record.status, "finalized");
11394        assert!(client.in_flight.lock().is_none());
11395
11396        drop_execution_schema(&admin_pool, &schema).await;
11397    }
11398
11399    #[tokio::test]
11400    async fn submit_order_persists_authenticated_envelope_before_broadcast() {
11401        let state = swap_rpc_state().await;
11402        let Some((admin_pool, schema, mut client, state, _)) =
11403            swap_client_with_database("protected_submit_test", state).await
11404        else {
11405            return;
11406        };
11407        let order = test_market_sell_order(test_pool().instrument_id);
11408        let mut receiver = start_with_events(&mut client);
11409
11410        client.submit_order(submit_order_cmd(&order)).unwrap();
11411        await_pending_tasks(&client).await;
11412
11413        let events = collect_order_events(&mut receiver);
11414        assert_swap_submitted_and_filled(&events);
11415        let representations: Vec<(bool, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
11416            "SELECT raw_transaction IS NULL, sealed_transaction IS NOT NULL \
11417             FROM {schema}.execution_transaction_hash WHERE payload_expected"
11418        )))
11419        .fetch_all(&admin_pool)
11420        .await
11421        .unwrap();
11422        let broadcasts = state
11423            .recorded_requests()
11424            .into_iter()
11425            .filter(|request| request["method"] == "eth_sendRawTransaction")
11426            .count();
11427        assert_eq!(representations, vec![(true, true)]);
11428        assert_eq!(broadcasts, 1);
11429
11430        drop_execution_schema(&admin_pool, &schema).await;
11431    }
11432
11433    #[tokio::test]
11434    async fn protected_persistence_failure_prevents_broadcast_and_acknowledgment() {
11435        let state = swap_rpc_state().await;
11436        let Some((admin_pool, schema, mut client, state, _)) =
11437            swap_client_with_database("protected_persist_failure_test", state).await
11438        else {
11439            return;
11440        };
11441
11442        for statement in [
11443            format!(
11444                "CREATE FUNCTION {schema}.reject_protected_payload() RETURNS trigger \
11445                 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test payload rejection''; END'"
11446            ),
11447            format!(
11448                "CREATE TRIGGER reject_protected_payload BEFORE INSERT ON \
11449                 {schema}.execution_transaction_hash FOR EACH ROW \
11450                 EXECUTE FUNCTION {schema}.reject_protected_payload()"
11451            ),
11452        ] {
11453            sqlx::query(sqlx::AssertSqlSafe(statement))
11454                .execute(&admin_pool)
11455                .await
11456                .unwrap();
11457        }
11458        let order = test_market_sell_order(test_pool().instrument_id);
11459        let mut receiver = start_with_events(&mut client);
11460
11461        client.submit_order(submit_order_cmd(&order)).unwrap();
11462        await_pending_tasks(&client).await;
11463
11464        let events = collect_order_events(&mut receiver);
11465        assert!(
11466            events
11467                .iter()
11468                .all(|event| !matches!(event, OrderEventAny::Submitted(_)))
11469        );
11470        let broadcasts = state
11471            .recorded_requests()
11472            .into_iter()
11473            .filter(|request| request["method"] == "eth_sendRawTransaction")
11474            .count();
11475        let payload_rows: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11476            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11477        )))
11478        .fetch_one(&admin_pool)
11479        .await
11480        .unwrap();
11481        assert_eq!(broadcasts, 0);
11482        assert_eq!(payload_rows, 0);
11483
11484        drop_execution_schema(&admin_pool, &schema).await;
11485    }
11486
11487    #[tokio::test]
11488    async fn submit_order_reservation_failure_denies_without_broadcast_and_releases_slot() {
11489        let Some((admin_pool, schema, mut client, state, cache)) =
11490            swap_client_with_database("execution_submit_persist_fail_test", swap_rpc_state().await)
11491                .await
11492        else {
11493            return;
11494        };
11495        sqlx::query(sqlx::AssertSqlSafe(format!(
11496            "DROP TABLE {schema}.execution_intent CASCADE"
11497        )))
11498        .execute(&admin_pool)
11499        .await
11500        .unwrap();
11501        let pool = test_pool();
11502        let first = test_market_sell_order(pool.instrument_id);
11503        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11504        cache
11505            .borrow_mut()
11506            .add_order(second.clone(), None, None, false)
11507            .unwrap();
11508        let mut receiver = start_with_events(&mut client);
11509
11510        client.submit_order(submit_order_cmd(&first)).unwrap();
11511        await_pending_tasks(&client).await;
11512
11513        let events = collect_order_events(&mut receiver);
11514        assert_eq!(events.len(), 1);
11515        let OrderEventAny::Denied(denied) = &events[0] else {
11516            panic!("expected OrderDenied, was {:?}", events[0]);
11517        };
11518        assert!(
11519            denied
11520                .reason
11521                .as_str()
11522                .contains("Execution intent reservation failed before commit"),
11523            "was: {}",
11524            denied.reason
11525        );
11526        client.submit_order(submit_order_cmd(&second)).unwrap();
11527        await_pending_tasks(&client).await;
11528        let retry_events = collect_order_events(&mut receiver);
11529        assert_eq!(retry_events.len(), 1);
11530        let OrderEventAny::Denied(retry_denied) = &retry_events[0] else {
11531            panic!("expected OrderDenied, was {:?}", retry_events[0]);
11532        };
11533        assert_eq!(
11534            retry_denied.reason.as_str(),
11535            "Execution intent reservation failed before commit"
11536        );
11537        let broadcasts = state
11538            .recorded_requests()
11539            .into_iter()
11540            .filter(|request| request["method"] == "eth_sendRawTransaction")
11541            .count();
11542        assert_eq!(broadcasts, 0);
11543        assert!(client.in_flight.lock().is_none());
11544
11545        drop_execution_schema(&admin_pool, &schema).await;
11546    }
11547
11548    #[tokio::test]
11549    async fn submit_order_reservation_commit_failure_keeps_preparing_slot() {
11550        let Some((admin_pool, schema, mut client, state, cache)) = swap_client_with_database(
11551            "execution_submit_reservation_commit_fail_test",
11552            swap_rpc_state().await,
11553        )
11554        .await
11555        else {
11556            return;
11557        };
11558        install_reservation_commit_rejection(&admin_pool, &schema).await;
11559        let pool = test_pool();
11560        let first = test_market_sell_order(pool.instrument_id);
11561        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11562        cache
11563            .borrow_mut()
11564            .add_order(second.clone(), None, None, false)
11565            .unwrap();
11566        let mut receiver = start_with_events(&mut client);
11567
11568        client.submit_order(submit_order_cmd(&first)).unwrap();
11569        await_pending_tasks(&client).await;
11570        client.submit_order(submit_order_cmd(&second)).unwrap();
11571        await_pending_tasks(&client).await;
11572
11573        let events = collect_order_events(&mut receiver);
11574        let submitted = events
11575            .iter()
11576            .filter(|event| matches!(event, OrderEventAny::Submitted(_)))
11577            .count();
11578        let denied_commit = events
11579            .iter()
11580            .filter(|event| {
11581                matches!(event, OrderEventAny::Denied(denied) if denied.reason.as_str() == "Execution intent reservation commit outcome is unknown; reconciliation is required")
11582            })
11583            .count();
11584        let denied_in_flight = events
11585            .iter()
11586            .filter(|event| {
11587                matches!(event, OrderEventAny::Denied(denied) if denied.reason.as_str().contains("at most one transaction can be in flight"))
11588            })
11589            .count();
11590        let requests = state.recorded_requests();
11591        let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11592            "SELECT COUNT(*) FROM {schema}.execution_intent"
11593        )))
11594        .fetch_one(&admin_pool)
11595        .await
11596        .unwrap();
11597        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11598            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11599        )))
11600        .fetch_one(&admin_pool)
11601        .await
11602        .unwrap();
11603
11604        assert_eq!(submitted, 0, "was: {events:?}");
11605        assert_eq!(denied_commit, 1, "was: {events:?}");
11606        assert_eq!(denied_in_flight, 1, "was: {events:?}");
11607        assert!(matches!(
11608            *client.in_flight.lock(),
11609            Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
11610        ));
11611        assert_eq!(intent_count, 0);
11612        assert_eq!(signed_count, 0);
11613        assert!(
11614            requests
11615                .iter()
11616                .all(|request| request["method"] != "eth_getTransactionCount")
11617        );
11618        assert!(
11619            requests
11620                .iter()
11621                .all(|request| request["method"] != "eth_sendRawTransaction")
11622        );
11623
11624        drop_execution_schema(&admin_pool, &schema).await;
11625    }
11626
11627    #[tokio::test]
11628    async fn protected_payload_migration_failure_keeps_plaintext_and_blocks_ready() {
11629        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
11630            "protected_payload_migration_failure",
11631            execution_rpc_state(),
11632        )
11633        .await
11634        else {
11635            return;
11636        };
11637        let database = client.cache.database.as_ref().unwrap();
11638        let (intent, _) = persist_invalid_test_swap(database, None).await;
11639        let keys = payload_test_keys([0x31; 32], vec![], "migration-failure");
11640        let error = database
11641            .ensure_execution_payload_storage(&keys)
11642            .await
11643            .unwrap_err();
11644
11645        assert!(
11646            error
11647                .to_string()
11648                .contains("failed to authenticate execution payload")
11649        );
11650        let row = database
11651            .get_execution_transaction_hashes(intent.id)
11652            .await
11653            .unwrap()
11654            .pop()
11655            .unwrap();
11656        let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11657            "SELECT operation FROM {schema}.execution_payload_state"
11658        )))
11659        .fetch_one(&admin_pool)
11660        .await
11661        .unwrap();
11662        assert_eq!(operation, "migrate");
11663        assert!(row.raw_transaction.is_some());
11664        assert!(row.sealed_transaction.is_none());
11665
11666        drop_execution_schema(&admin_pool, &schema).await;
11667    }
11668
11669    #[tokio::test]
11670    async fn protected_payload_migration_restart_restore_rewrap_and_rollback() {
11671        let Some((admin_pool, pg_config)) =
11672            connect_test_postgres("protected payload lifecycle").await
11673        else {
11674            return;
11675        };
11676        let schema = format!("protected_payload_lifecycle_{}", std::process::id());
11677        setup_execution_schema(&admin_pool, &schema).await;
11678        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11679        let options = options.options([("search_path", schema.clone())]);
11680        let database = connect_test_database(options.clone()).await.unwrap();
11681        database
11682            .ensure_execution_transaction_schema()
11683            .await
11684            .unwrap();
11685        let intent = reserve_test_wrap_intent(&database).await;
11686        database
11687            .assign_execution_intent_nonce(intent.id, 7)
11688            .await
11689            .unwrap();
11690        let transaction = build_eip1559_transaction(
11691            42161,
11692            7,
11693            78_000,
11694            130_000_000,
11695            10_000_000,
11696            WETH_ADDRESS,
11697            U256::from(1_u64),
11698            Bytes::from(hex::decode("d0e30db0").unwrap()),
11699        );
11700        let (transaction_hash, raw_transaction) = sign_eip1559_transaction(
11701            transaction,
11702            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
11703        )
11704        .await
11705        .unwrap();
11706        database
11707            .add_execution_transaction_hash(
11708                intent.id,
11709                42161,
11710                &transaction_hash.to_string(),
11711                &raw_transaction,
11712            )
11713            .await
11714            .unwrap();
11715        let keys = payload_test_keys([0x41; 32], vec![], "restore-a");
11716
11717        database
11718            .ensure_execution_payload_storage(&keys)
11719            .await
11720            .unwrap();
11721        let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11722            "SELECT operation FROM {schema}.execution_payload_state"
11723        )))
11724        .fetch_one(&admin_pool)
11725        .await
11726        .unwrap();
11727        assert_eq!(operation, "ready");
11728        drop(database);
11729
11730        let restarted = connect_test_database(options).await.unwrap();
11731        restarted
11732            .ensure_execution_payload_storage(&keys)
11733            .await
11734            .unwrap();
11735        let row = restarted
11736            .get_execution_transaction_hashes(intent.id)
11737            .await
11738            .unwrap()
11739            .pop()
11740            .unwrap();
11741        assert!(row.raw_transaction.is_none());
11742        let sealed_transaction = row.sealed_transaction.clone().unwrap();
11743        let stored_intent = restarted.get_execution_intent(intent.id).await.unwrap();
11744        let context = payload_context(&stored_intent, &row, keys.deployment_id()).unwrap();
11745        let alternate_envelope = keys.seal(&raw_transaction, &context).unwrap();
11746        assert_ne!(alternate_envelope, sealed_transaction);
11747        let repeated = restarted
11748            .add_execution_transaction_envelope(
11749                intent.id,
11750                42161,
11751                &transaction_hash.to_string(),
11752                &alternate_envelope,
11753            )
11754            .await
11755            .unwrap();
11756        let check = restarted
11757            .check_execution_payload_storage(Some(&keys), None, 1)
11758            .await
11759            .unwrap();
11760        assert_eq!(
11761            repeated.sealed_transaction,
11762            Some(sealed_transaction.clone())
11763        );
11764        assert_eq!(check.plaintext_rows, 0);
11765        assert_eq!(check.original_rows, 1);
11766        assert_eq!(check.replacement_rows, 0);
11767        assert_eq!(check.authenticated_rows, 1);
11768        assert!(!check.read_roles.is_empty());
11769
11770        sqlx::query(sqlx::AssertSqlSafe(format!(
11771            "UPDATE {schema}.execution_payload_key_state SET seals = 4294967295"
11772        )))
11773        .execute(&admin_pool)
11774        .await
11775        .unwrap();
11776        let exhausted = restarted
11777            .reserve_execution_payload_seal(&keys)
11778            .await
11779            .unwrap_err();
11780        assert!(exhausted.to_string().contains("seal limit"));
11781        let seals: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11782            "SELECT seals FROM {schema}.execution_payload_key_state"
11783        )))
11784        .fetch_one(&admin_pool)
11785        .await
11786        .unwrap();
11787        assert_eq!(seals, 4_294_967_295);
11788        sqlx::query(sqlx::AssertSqlSafe(format!(
11789            "UPDATE {schema}.execution_payload_key_state SET seals = 1"
11790        )))
11791        .execute(&admin_pool)
11792        .await
11793        .unwrap();
11794
11795        let missing_key = restarted
11796            .check_execution_payload_storage(None, None, 1)
11797            .await
11798            .unwrap_err();
11799        assert!(
11800            missing_key
11801                .to_string()
11802                .contains("no payload key is configured")
11803        );
11804        let restored_elsewhere = payload_test_keys([0x41; 32], vec![], "restore-b");
11805        let wrong_context = restarted
11806            .ensure_execution_payload_storage(&restored_elsewhere)
11807            .await
11808            .unwrap_err();
11809        assert!(wrong_context.to_string().contains("deployment ID"));
11810
11811        sqlx::query(sqlx::AssertSqlSafe(format!(
11812            "UPDATE {schema}.execution_transaction_hash \
11813             SET sealed_transaction = set_byte(sealed_transaction, octet_length(sealed_transaction) - 1, \
11814                 get_byte(sealed_transaction, octet_length(sealed_transaction) - 1) # 1)"
11815        )))
11816        .execute(&admin_pool)
11817        .await
11818        .unwrap();
11819        assert!(
11820            restarted
11821                .check_execution_payload_storage(Some(&keys), None, 1)
11822                .await
11823                .is_err()
11824        );
11825        sqlx::query(sqlx::AssertSqlSafe(format!(
11826            "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11827        )))
11828        .bind(&sealed_transaction)
11829        .execute(&admin_pool)
11830        .await
11831        .unwrap();
11832
11833        for statement in [
11834            format!(
11835                "ALTER TABLE {schema}.execution_transaction_hash \
11836                 DROP CONSTRAINT execution_transaction_payload_protected_check"
11837            ),
11838            format!("UPDATE {schema}.execution_transaction_hash SET sealed_transaction = NULL"),
11839        ] {
11840            sqlx::query(sqlx::AssertSqlSafe(statement))
11841                .execute(&admin_pool)
11842                .await
11843                .unwrap();
11844        }
11845        assert!(
11846            restarted
11847                .check_execution_payload_storage(Some(&keys), None, 1)
11848                .await
11849                .is_err()
11850        );
11851        sqlx::query(sqlx::AssertSqlSafe(format!(
11852            "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11853        )))
11854        .bind(&sealed_transaction)
11855        .execute(&admin_pool)
11856        .await
11857        .unwrap();
11858        sqlx::query(sqlx::AssertSqlSafe(format!(
11859            "ALTER TABLE {schema}.execution_transaction_hash \
11860             ADD CONSTRAINT execution_transaction_payload_protected_check CHECK ( \
11861                 (payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NOT NULL) \
11862                 OR (NOT payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NULL) \
11863             )"
11864        )))
11865        .execute(&admin_pool)
11866        .await
11867        .unwrap();
11868
11869        let rotated = payload_test_keys([0x52; 32], vec![[0x41; 32]], "restore-a");
11870        restarted
11871            .rewrap_execution_payload_storage(&rotated, 1)
11872            .await
11873            .unwrap();
11874        let rotated_check = restarted
11875            .check_execution_payload_storage(Some(&rotated), None, 1)
11876            .await
11877            .unwrap();
11878        assert_eq!(rotated_check.authenticated_rows, 1);
11879        assert_eq!(rotated_check.key_ids.len(), 1);
11880
11881        restarted
11882            .rollback_execution_payload_storage(&rotated, 1)
11883            .await
11884            .unwrap();
11885        let rolled_back = restarted
11886            .get_execution_transaction_hashes(intent.id)
11887            .await
11888            .unwrap()
11889            .pop()
11890            .unwrap();
11891        assert_eq!(
11892            rolled_back.raw_transaction.as_deref(),
11893            Some(raw_transaction.as_slice())
11894        );
11895        assert!(rolled_back.sealed_transaction.is_none());
11896        let legacy_check = restarted
11897            .check_execution_payload_storage(None, None, 1)
11898            .await
11899            .unwrap();
11900        assert!(!legacy_check.protected);
11901        assert_eq!(legacy_check.plaintext_rows, 1);
11902        assert_eq!(legacy_check.authenticated_rows, 1);
11903
11904        drop_execution_schema(&admin_pool, &schema).await;
11905    }
11906
11907    #[tokio::test]
11908    async fn protected_payload_storage_authenticates_multiple_execution_identities() {
11909        let Some((admin_pool, pg_config)) =
11910            connect_test_postgres("protected payload multiple identities").await
11911        else {
11912            return;
11913        };
11914        let schema = format!("protected_payload_identities_{}", std::process::id());
11915        setup_execution_schema(&admin_pool, &schema).await;
11916        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11917        let database = connect_test_database(options.options([("search_path", schema.clone())]))
11918            .await
11919            .unwrap();
11920        database
11921            .ensure_execution_transaction_schema()
11922            .await
11923            .unwrap();
11924
11925        let private_keys = [
11926            TEST_PRIVATE_KEY,
11927            "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
11928        ];
11929
11930        for (index, private_key) in private_keys.into_iter().enumerate() {
11931            let signer = PrivateKeySigner::from_str(private_key).unwrap();
11932            let nonce = 7 + u64::try_from(index).unwrap();
11933            let intent =
11934                reserve_test_wrap_intent_for_wallet(&database, &signer.address().to_string()).await;
11935            database
11936                .assign_execution_intent_nonce(intent.id, nonce)
11937                .await
11938                .unwrap();
11939            let transaction = build_eip1559_transaction(
11940                42161,
11941                nonce,
11942                78_000,
11943                130_000_000,
11944                10_000_000,
11945                WETH_ADDRESS,
11946                U256::from(1_u64),
11947                Bytes::from(hex::decode("d0e30db0").unwrap()),
11948            );
11949            let (transaction_hash, raw_transaction) =
11950                sign_eip1559_transaction(transaction, &signer)
11951                    .await
11952                    .unwrap();
11953            database
11954                .add_execution_transaction_hash(
11955                    intent.id,
11956                    42161,
11957                    &transaction_hash.to_string(),
11958                    &raw_transaction,
11959                )
11960                .await
11961                .unwrap();
11962        }
11963
11964        let keys = payload_test_keys([0x61; 32], vec![], "multiple-identities");
11965        database
11966            .ensure_execution_payload_storage(&keys)
11967            .await
11968            .unwrap();
11969        let lease = database
11970            .require_execution_payload_storage(
11971                &keys,
11972                PayloadPolicy {
11973                    chain_id: 42161,
11974                    signer: Address::from_str(WALLET).unwrap(),
11975                    gas_limit: 1_000_000,
11976                    max_fee_per_gas: 1_000_000_000,
11977                },
11978                1,
11979            )
11980            .await
11981            .unwrap();
11982        drop(lease);
11983        let check = database
11984            .check_execution_payload_storage(Some(&keys), None, 1)
11985            .await
11986            .unwrap();
11987
11988        assert!(check.protected);
11989        assert_eq!(check.plaintext_rows, 0);
11990        assert_eq!(check.original_rows, 2);
11991        assert_eq!(check.replacement_rows, 0);
11992        assert_eq!(check.authenticated_rows, 2);
11993        assert_eq!(check.key_ids.len(), 1);
11994
11995        drop_execution_schema(&admin_pool, &schema).await;
11996    }
11997
11998    #[rstest]
11999    #[case::finalized(TransactionStatus::Finalized)]
12000    #[case::reverted(TransactionStatus::Reverted)]
12001    #[tokio::test]
12002    async fn protected_payload_storage_ignores_current_policy_for_released_terminal_history(
12003        #[case] status: TransactionStatus,
12004    ) {
12005        let Some((admin_pool, pg_config)) =
12006            connect_test_postgres("protected payload terminal history").await
12007        else {
12008            return;
12009        };
12010        let schema = format!(
12011            "protected_payload_terminal_{}_{}",
12012            status.as_str(),
12013            std::process::id()
12014        );
12015        setup_execution_schema(&admin_pool, &schema).await;
12016        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
12017        let database = connect_test_database(options.options([("search_path", schema.clone())]))
12018            .await
12019            .unwrap();
12020        database
12021            .ensure_execution_transaction_schema()
12022            .await
12023            .unwrap();
12024        let (intent, transaction_hash, _) = persist_test_wrap_broadcast(&database, None).await;
12025        database
12026            .record_execution_status(
12027                intent.id,
12028                &transaction_hash.to_string(),
12029                status,
12030                None,
12031                None,
12032                None,
12033                None,
12034                None,
12035            )
12036            .await
12037            .unwrap();
12038        database
12039            .mark_execution_event_emitted(intent.id, "terminal")
12040            .await
12041            .unwrap();
12042        let keys = payload_test_keys([0x62; 32], vec![], "terminal-history");
12043        database
12044            .ensure_execution_payload_storage(&keys)
12045            .await
12046            .unwrap();
12047
12048        let lease = database
12049            .require_execution_payload_storage(
12050                &keys,
12051                PayloadPolicy {
12052                    chain_id: 42161,
12053                    signer: Address::from_str(WALLET).unwrap(),
12054                    gas_limit: 1,
12055                    max_fee_per_gas: 1,
12056                },
12057                1,
12058            )
12059            .await
12060            .unwrap();
12061        drop(lease);
12062        let check = database
12063            .check_execution_payload_storage(Some(&keys), None, 1)
12064            .await
12065            .unwrap();
12066        let active_intent = reserve_test_wrap_intent(&database).await;
12067        database
12068            .assign_execution_intent_nonce(active_intent.id, 8)
12069            .await
12070            .unwrap();
12071        let active_intent = database
12072            .get_execution_intent(active_intent.id)
12073            .await
12074            .unwrap();
12075        let active_transaction = build_eip1559_transaction(
12076            42161,
12077            8,
12078            78_000,
12079            130_000_000,
12080            10_000_000,
12081            WETH_ADDRESS,
12082            U256::from(1_u64),
12083            Bytes::from(hex::decode("d0e30db0").unwrap()),
12084        );
12085        let (active_hash, active_raw_transaction) = sign_eip1559_transaction(
12086            active_transaction,
12087            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
12088        )
12089        .await
12090        .unwrap();
12091        persist_test_payload(
12092            &database,
12093            Some(&keys),
12094            &active_intent,
12095            active_hash,
12096            &active_raw_transaction,
12097        )
12098        .await;
12099        let active_error = match database
12100            .require_execution_payload_storage(
12101                &keys,
12102                PayloadPolicy {
12103                    chain_id: 42161,
12104                    signer: Address::from_str(WALLET).unwrap(),
12105                    gas_limit: 1,
12106                    max_fee_per_gas: 1,
12107                },
12108                1,
12109            )
12110            .await
12111        {
12112            Ok(_) => panic!("active execution payload unexpectedly passed current policy"),
12113            Err(e) => e,
12114        };
12115        let active_error = format!("{active_error:#}");
12116
12117        assert!(check.protected);
12118        assert_eq!(check.authenticated_rows, 1);
12119        assert!(
12120            active_error.contains(&format!(
12121                "execution intent {} transaction {active_hash} violates current execution policy",
12122                active_intent.id
12123            )),
12124            "was: {active_error}"
12125        );
12126        assert!(
12127            active_error.contains("gas limit 78000 exceeds configured ceiling 1"),
12128            "was: {active_error}"
12129        );
12130
12131        drop_execution_schema(&admin_pool, &schema).await;
12132    }
12133
12134    #[allow(unsafe_code)] // env-var mutation uses the test client's dedicated signer variable
12135    #[tokio::test]
12136    async fn rollback_blocks_execution_until_protection_and_full_check_succeed() {
12137        let state = ready_rpc_state();
12138        let Some((admin_pool, schema, mut client, state)) =
12139            execution_client_with_database("payload_rollback_reactivation", state).await
12140        else {
12141            return;
12142        };
12143        // SAFETY: this test binary owns the dedicated test signer variable
12144        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
12145        client.disconnect().await.unwrap();
12146        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
12147        replace_exec_event_sender(sender);
12148        client.start().unwrap();
12149
12150        client.rollback_payload_storage(1).await.unwrap();
12151        let unprotected = client.check_payload_storage(1).await.unwrap();
12152        let error = client.connect().await.unwrap_err();
12153
12154        assert!(!unprotected.protected);
12155        assert_eq!(unprotected.plaintext_rows, 0);
12156        assert_eq!(unprotected.authenticated_rows, 0);
12157        assert!(
12158            error
12159                .to_string()
12160                .contains("Postgres execution requires protected payload storage"),
12161            "was: {error}"
12162        );
12163        assert!(client.signer.is_none());
12164        assert!(state.recorded_requests().is_empty());
12165
12166        client.protect_payload_storage().await.unwrap();
12167        let protected = client.check_payload_storage(1).await.unwrap();
12168        assert!(protected.protected);
12169        assert_eq!(protected.plaintext_rows, 0);
12170        assert_eq!(protected.authenticated_rows, 0);
12171
12172        client.connect().await.unwrap();
12173
12174        assert!(client.is_connected());
12175        assert!(client.transaction_executor().is_ok());
12176        assert!(!state.recorded_requests().is_empty());
12177
12178        drop(client);
12179        drop_execution_schema(&admin_pool, &schema).await;
12180    }
12181
12182    #[rstest]
12183    #[case::active_key(true)]
12184    #[case::deployment_identity(false)]
12185    #[tokio::test]
12186    async fn postgres_connect_rejects_missing_payload_identity_before_rpc(
12187        #[case] remove_active_key: bool,
12188    ) {
12189        let test_name = if remove_active_key {
12190            "payload_connect_missing_active_key"
12191        } else {
12192            "payload_connect_missing_deployment"
12193        };
12194        let Some((admin_pool, schema, mut client, state)) =
12195            execution_client_with_database(test_name, ready_rpc_state()).await
12196        else {
12197            return;
12198        };
12199        client.disconnect().await.unwrap();
12200        client.payload_keys = None;
12201        if remove_active_key {
12202            client.config.payload_key_env = None;
12203            client.config.payload_deployment_id = None;
12204        } else {
12205            client.config.payload_deployment_id = None;
12206        }
12207
12208        let error = client.connect().await.unwrap_err();
12209
12210        assert!(
12211            error.to_string().contains(if remove_active_key {
12212                "Postgres execution requires an active payload key and deployment identity"
12213            } else {
12214                "Payload deployment ID is required"
12215            }),
12216            "was: {error}"
12217        );
12218        assert!(client.signer.is_none());
12219        assert!(client.payload_keys.is_none());
12220        assert!(state.recorded_requests().is_empty());
12221
12222        drop(client);
12223        drop_execution_schema(&admin_pool, &schema).await;
12224    }
12225
12226    #[tokio::test]
12227    async fn postgres_connect_rejects_missing_envelope_key_before_rpc() {
12228        let Some((admin_pool, schema, mut client, state)) = execution_client_with_database(
12229            "payload_connect_missing_envelope_key",
12230            ready_rpc_state(),
12231        )
12232        .await
12233        else {
12234            return;
12235        };
12236        let database = client.cache.database.as_ref().unwrap().clone();
12237        persist_test_wrap_broadcast(&database, client.payload_keys.as_deref()).await;
12238        client.disconnect().await.unwrap();
12239        client.payload_keys = None;
12240        let keys = set_test_payload_key(&mut client, [0xb6; 32], &schema);
12241        sqlx::query(sqlx::AssertSqlSafe(format!(
12242            "UPDATE {schema}.execution_payload_state SET active_key_id = $1 \
12243             WHERE component = 'signed_transactions'"
12244        )))
12245        .bind(keys.active_key_id().as_slice())
12246        .execute(&admin_pool)
12247        .await
12248        .unwrap();
12249
12250        let error = client.connect().await.unwrap_err();
12251
12252        assert!(
12253            error
12254                .to_string()
12255                .contains("Stored execution payload requires unavailable key"),
12256            "was: {error}"
12257        );
12258        assert!(client.signer.is_none());
12259        assert!(client.payload_keys.is_none());
12260        assert!(state.recorded_requests().is_empty());
12261
12262        drop(client);
12263        drop_execution_schema(&admin_pool, &schema).await;
12264    }
12265
12266    #[tokio::test]
12267    async fn payload_action_lease_blocks_rewrap_transition_until_release() {
12268        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
12269            "payload_action_lease",
12270            execution_rpc_state(),
12271        )
12272        .await
12273        else {
12274            return;
12275        };
12276        let database = client.cache.database.as_ref().unwrap();
12277        let keys = payload_test_keys([0x71; 32], vec![], "action-lease");
12278        database
12279            .ensure_execution_payload_storage(&keys)
12280            .await
12281            .unwrap();
12282        let lease = database
12283            .acquire_execution_payload_lease(&keys)
12284            .await
12285            .unwrap();
12286        let rotated = payload_test_keys([0x72; 32], vec![[0x71; 32]], "action-lease");
12287        let mut operation = Box::pin(database.rewrap_execution_payload_storage(&rotated, 1));
12288
12289        assert!(
12290            tokio::time::timeout(Duration::from_millis(50), &mut operation)
12291                .await
12292                .is_err()
12293        );
12294        let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12295            "SELECT operation FROM {schema}.execution_payload_state"
12296        )))
12297        .fetch_one(&admin_pool)
12298        .await
12299        .unwrap();
12300        assert_eq!(state, "ready");
12301
12302        drop(lease);
12303        tokio::time::timeout(Duration::from_secs(2), operation)
12304            .await
12305            .unwrap()
12306            .unwrap();
12307        let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12308            "SELECT operation FROM {schema}.execution_payload_state"
12309        )))
12310        .fetch_one(&admin_pool)
12311        .await
12312        .unwrap();
12313        assert_eq!(state, "ready");
12314
12315        drop_execution_schema(&admin_pool, &schema).await;
12316    }
12317
12318    #[tokio::test]
12319    async fn payload_infrastructure_checks_are_scoped_to_the_execution_schema() {
12320        let Some((admin_pool, pg_config)) = connect_test_postgres("payload schema isolation").await
12321        else {
12322            return;
12323        };
12324        let schema_a = format!("payload_schema_a_{}", std::process::id());
12325        let schema_b = format!("payload_schema_b_{}", std::process::id());
12326        setup_execution_schema(&admin_pool, &schema_a).await;
12327        setup_execution_schema(&admin_pool, &schema_b).await;
12328        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
12329        let database_a =
12330            connect_test_database(options.clone().options([("search_path", schema_a.clone())]))
12331                .await
12332                .unwrap();
12333        let database_b =
12334            connect_test_database(options.options([("search_path", schema_b.clone())]))
12335                .await
12336                .unwrap();
12337        database_a
12338            .ensure_execution_transaction_schema()
12339            .await
12340            .unwrap();
12341        database_b
12342            .ensure_execution_transaction_schema()
12343            .await
12344            .unwrap();
12345        let keys_a = payload_test_keys([0x91; 32], vec![], "schema-a");
12346        let keys_b = payload_test_keys([0x92; 32], vec![], "schema-b");
12347
12348        database_a
12349            .ensure_execution_payload_storage(&keys_a)
12350            .await
12351            .unwrap();
12352        database_b
12353            .ensure_execution_payload_storage(&keys_b)
12354            .await
12355            .unwrap();
12356
12357        for statement in [
12358            format!(
12359                "DROP TRIGGER execution_transaction_payload_fence ON \
12360                 {schema_a}.execution_transaction_hash"
12361            ),
12362            format!(
12363                "ALTER TABLE {schema_a}.execution_transaction_hash \
12364                 DROP CONSTRAINT execution_transaction_payload_protected_check"
12365            ),
12366        ] {
12367            sqlx::query(sqlx::AssertSqlSafe(statement))
12368                .execute(&admin_pool)
12369                .await
12370                .unwrap();
12371        }
12372
12373        let error = database_a
12374            .ensure_execution_payload_storage(&keys_a)
12375            .await
12376            .unwrap_err();
12377
12378        assert!(
12379            error
12380                .to_string()
12381                .contains("without its write fence or constraint"),
12382            "was: {error}"
12383        );
12384
12385        drop_execution_schema(&admin_pool, &schema_a).await;
12386        drop_execution_schema(&admin_pool, &schema_b).await;
12387    }
12388
12389    #[tokio::test]
12390    async fn execution_fresh_schema_preserves_nullable_wallet_address() {
12391        let Some((admin_pool, _)) = connect_test_postgres("fresh execution schema").await else {
12392            return;
12393        };
12394        let schema = format!("execution_fresh_test_{}", std::process::id());
12395        let mut transaction = admin_pool.begin().await.unwrap();
12396        sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {schema}")))
12397            .execute(&mut *transaction)
12398            .await
12399            .unwrap();
12400        sqlx::query(sqlx::AssertSqlSafe(format!(
12401            "SET LOCAL search_path TO {schema}"
12402        )))
12403        .execute(&mut *transaction)
12404        .await
12405        .unwrap();
12406        sqlx::query("CREATE TABLE chain (chain_id INTEGER PRIMARY KEY)")
12407            .execute(&mut *transaction)
12408            .await
12409            .unwrap();
12410        sqlx::query("INSERT INTO chain (chain_id) VALUES (42161)")
12411            .execute(&mut *transaction)
12412            .await
12413            .unwrap();
12414        sqlx::query(sqlx::AssertSqlSafe(execution_transaction_create_sql()))
12415            .execute(&mut *transaction)
12416            .await
12417            .unwrap();
12418        sqlx::query(
12419            "INSERT INTO execution_transaction \
12420             (chain_id, nonce, transaction_hash, purpose, status) \
12421             VALUES (42161, 7, '0xfresh-legacy', 'wrap', 'rejected')",
12422        )
12423        .execute(&mut *transaction)
12424        .await
12425        .unwrap();
12426
12427        let is_nullable: String = sqlx::query_scalar(
12428            "SELECT is_nullable FROM information_schema.columns \
12429             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12430             AND column_name = 'wallet_address'",
12431        )
12432        .bind(&schema)
12433        .fetch_one(&mut *transaction)
12434        .await
12435        .unwrap();
12436        let wallet_address: Option<String> = sqlx::query_scalar(
12437            "SELECT wallet_address FROM execution_transaction \
12438             WHERE transaction_hash = '0xfresh-legacy'",
12439        )
12440        .fetch_one(&mut *transaction)
12441        .await
12442        .unwrap();
12443
12444        assert_eq!(is_nullable, "YES");
12445        assert_eq!(wallet_address, None);
12446        transaction.rollback().await.unwrap();
12447    }
12448
12449    #[tokio::test]
12450    async fn execution_schema_migration_preserves_existing_rows() {
12451        let Some((admin_pool, pg_config)) =
12452            connect_test_postgres("execution schema migration").await
12453        else {
12454            return;
12455        };
12456        let schema = format!("execution_migration_test_{}", std::process::id());
12457        setup_execution_schema(&admin_pool, &schema).await;
12458        sqlx::query(sqlx::AssertSqlSafe(format!(
12459            "INSERT INTO {schema}.execution_transaction \
12460             (chain_id, nonce, transaction_hash, purpose, status) \
12461             VALUES (42161, 7, '0xlegacy', 'wrap', 'rejected')"
12462        )))
12463        .execute(&admin_pool)
12464        .await
12465        .unwrap();
12466
12467        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12468        let db_options = db_options.options([("search_path", schema.clone())]);
12469        let database = connect_test_database(db_options).await.unwrap();
12470        database
12471            .ensure_execution_transaction_schema()
12472            .await
12473            .unwrap();
12474        let legacy = database
12475            .get_execution_transaction(42161, "0xlegacy")
12476            .await
12477            .unwrap()
12478            .unwrap();
12479        assert_eq!(legacy.purpose, "wrap");
12480        assert_eq!(legacy.status, "rejected");
12481        assert_eq!(legacy.client_order_id, None);
12482        assert_eq!(legacy.wallet_address, None);
12483        let is_nullable: String = sqlx::query_scalar(
12484            "SELECT is_nullable FROM information_schema.columns \
12485             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12486             AND column_name = 'wallet_address'",
12487        )
12488        .bind(&schema)
12489        .fetch_one(&admin_pool)
12490        .await
12491        .unwrap();
12492        let fence_error = database
12493            .add_execution_transaction(
12494                42161,
12495                WALLET,
12496                8,
12497                "0xswap",
12498                "swap",
12499                "pending",
12500                Some("O-SWAP-001"),
12501            )
12502            .await
12503            .err()
12504            .unwrap();
12505        assert!(
12506            fence_error
12507                .to_string()
12508                .contains("Legacy execution writer refused"),
12509            "was: {fence_error}"
12510        );
12511        assert_eq!(is_nullable, "YES");
12512
12513        drop_execution_schema(&admin_pool, &schema).await;
12514    }
12515
12516    #[tokio::test]
12517    async fn execution_schema_migration_drops_legacy_wallet_not_null() {
12518        let Some((admin_pool, pg_config)) =
12519            connect_test_postgres("legacy wallet address schema migration").await
12520        else {
12521            return;
12522        };
12523        let schema = format!("execution_wallet_migration_test_{}", std::process::id());
12524        setup_execution_schema(&admin_pool, &schema).await;
12525        sqlx::query(sqlx::AssertSqlSafe(format!(
12526            "ALTER TABLE {schema}.execution_transaction \
12527             ADD COLUMN wallet_address TEXT NOT NULL"
12528        )))
12529        .execute(&admin_pool)
12530        .await
12531        .unwrap();
12532        sqlx::query(sqlx::AssertSqlSafe(format!(
12533            "INSERT INTO {schema}.execution_transaction \
12534             (chain_id, wallet_address, nonce, transaction_hash, purpose, status) \
12535             VALUES (42161, '{WALLET}', 7, '0xlegacy-wallet', 'wrap', 'rejected')"
12536        )))
12537        .execute(&admin_pool)
12538        .await
12539        .unwrap();
12540
12541        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12542        let db_options = db_options.options([("search_path", schema.clone())]);
12543        let database = connect_test_database(db_options).await.unwrap();
12544        database
12545            .ensure_execution_transaction_schema()
12546            .await
12547            .unwrap();
12548        let legacy = database
12549            .get_execution_transaction(42161, "0xlegacy-wallet")
12550            .await
12551            .unwrap()
12552            .unwrap();
12553        let is_nullable: String = sqlx::query_scalar(
12554            "SELECT is_nullable FROM information_schema.columns \
12555             WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12556             AND column_name = 'wallet_address'",
12557        )
12558        .bind(&schema)
12559        .fetch_one(&admin_pool)
12560        .await
12561        .unwrap();
12562
12563        assert_eq!(legacy.wallet_address.as_deref(), Some(WALLET));
12564        assert_eq!(is_nullable, "YES");
12565        drop_execution_schema(&admin_pool, &schema).await;
12566    }
12567
12568    #[tokio::test]
12569    async fn execution_schema_migration_refuses_unresolved_legacy_rows() {
12570        let Some((admin_pool, pg_config)) =
12571            connect_test_postgres("unsafe execution schema migration").await
12572        else {
12573            return;
12574        };
12575        let schema = format!("execution_unsafe_migration_test_{}", std::process::id());
12576        setup_execution_schema(&admin_pool, &schema).await;
12577        sqlx::query(sqlx::AssertSqlSafe(format!(
12578            "INSERT INTO {schema}.execution_transaction \
12579             (chain_id, nonce, transaction_hash, purpose, status) \
12580             VALUES (42161, 7, '0xunresolved', 'wrap', 'pending')"
12581        )))
12582        .execute(&admin_pool)
12583        .await
12584        .unwrap();
12585        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12586        let db_options = db_options.options([("search_path", schema.clone())]);
12587        let database = connect_test_database(db_options).await.unwrap();
12588
12589        let error = database
12590            .ensure_execution_transaction_schema()
12591            .await
12592            .unwrap_err();
12593        let legacy_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12594            "SELECT COUNT(*) FROM {schema}.execution_transaction \
12595             WHERE transaction_hash = '0xunresolved' AND status = 'pending'"
12596        )))
12597        .fetch_one(&admin_pool)
12598        .await
12599        .unwrap();
12600        let v2_table: Option<String> = sqlx::query_scalar("SELECT to_regclass($1)::TEXT")
12601            .bind(format!("{schema}.execution_intent"))
12602            .fetch_one(&admin_pool)
12603            .await
12604            .unwrap();
12605
12606        assert!(
12607            error
12608                .to_string()
12609                .contains("Cannot safely migrate 1 unresolved execution schema version 1"),
12610            "was: {error}"
12611        );
12612        assert_eq!(legacy_count, 1);
12613        assert_eq!(v2_table, None);
12614
12615        drop_execution_schema(&admin_pool, &schema).await;
12616    }
12617
12618    #[rstest]
12619    fn receipt_max_polls_derives_from_timeout() {
12620        assert_eq!(receipt_max_polls(0), 1);
12621        assert_eq!(receipt_max_polls(1), 1);
12622        assert_eq!(receipt_max_polls(60), 60);
12623        assert_eq!(receipt_max_polls(u64::MAX), u32::MAX);
12624        assert_eq!(receipt_timeout(0), Duration::from_secs(1));
12625        assert_eq!(receipt_timeout(60), Duration::from_secs(60));
12626        assert_eq!(
12627            receipt_timeout(u64::MAX),
12628            Duration::from_secs(u64::from(u32::MAX))
12629        );
12630    }
12631
12632    #[rstest]
12633    fn submit_order_errors_when_order_not_cached() {
12634        let client = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string())).0;
12635        let mut cmd = submit_order_cmd(&test_market_sell_order(test_pool().instrument_id));
12636        cmd.client_order_id = ClientOrderId::from("O-UNKNOWN");
12637
12638        let error = client.submit_order(cmd).unwrap_err();
12639
12640        assert!(!error.to_string().is_empty());
12641    }
12642
12643    #[tokio::test]
12644    async fn submit_order_denies_pool_fee_above_uint24() {
12645        let cache = Rc::new(RefCell::new(Cache::default()));
12646        let mut pool = test_pool();
12647        pool.fee = Some(16_777_216); // 2^24, above the uint24 calldata range
12648        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12649        let order = test_market_sell_order(pool.instrument_id);
12650        cache
12651            .borrow_mut()
12652            .add_order(order.clone(), None, None, false)
12653            .unwrap();
12654        let core = ExecutionClientCore::new(
12655            TraderId::from("TRADER-001"),
12656            ClientId::from("BLOCKCHAIN-001"),
12657            *BLOCKCHAIN_VENUE,
12658            OmsType::Netting,
12659            AccountId::from("BLOCKCHAIN-001"),
12660            AccountType::Wallet,
12661            None,
12662            cache,
12663        );
12664        let mut client =
12665            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12666                .unwrap();
12667        let mut receiver = start_with_events(&mut client);
12668
12669        client.submit_order(submit_order_cmd(&order)).unwrap();
12670
12671        let events = collect_order_events(&mut receiver);
12672        assert_eq!(events.len(), 1);
12673        let OrderEventAny::Denied(denied) = &events[0] else {
12674            panic!("expected OrderDenied, was {:?}", events[0]);
12675        };
12676        assert!(
12677            denied.reason.as_str().contains("exceeds uint24"),
12678            "was: {}",
12679            denied.reason
12680        );
12681    }
12682
12683    #[tokio::test]
12684    async fn submit_order_denies_uninitialized_profiler() {
12685        let cache = Rc::new(RefCell::new(Cache::default()));
12686        let pool = test_pool();
12687        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12688        let order = test_market_sell_order(pool.instrument_id);
12689        cache
12690            .borrow_mut()
12691            .add_order(order.clone(), None, None, false)
12692            .unwrap();
12693        cache
12694            .borrow_mut()
12695            .add_pool_profiler(PoolProfiler::new(Arc::new(pool)))
12696            .unwrap();
12697        let core = ExecutionClientCore::new(
12698            TraderId::from("TRADER-001"),
12699            ClientId::from("BLOCKCHAIN-001"),
12700            *BLOCKCHAIN_VENUE,
12701            OmsType::Netting,
12702            AccountId::from("BLOCKCHAIN-001"),
12703            AccountType::Wallet,
12704            None,
12705            cache,
12706        );
12707        let mut client =
12708            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12709                .unwrap();
12710        let mut receiver = start_with_events(&mut client);
12711
12712        client.submit_order(submit_order_cmd(&order)).unwrap();
12713
12714        let events = collect_order_events(&mut receiver);
12715        assert_eq!(events.len(), 1);
12716        let OrderEventAny::Denied(denied) = &events[0] else {
12717            panic!("expected OrderDenied, was {:?}", events[0]);
12718        };
12719        assert!(
12720            denied.reason.as_str().contains("is not initialized"),
12721            "was: {}",
12722            denied.reason
12723        );
12724    }
12725
12726    #[tokio::test]
12727    async fn submit_order_denies_when_quote_cannot_fill_order() {
12728        let cache = Rc::new(RefCell::new(Cache::default()));
12729        let pool = test_pool();
12730        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12731        let order = test_market_sell_order(pool.instrument_id);
12732        cache
12733            .borrow_mut()
12734            .add_order(order.clone(), None, None, false)
12735            .unwrap();
12736        // A narrow one-unit-of-liquidity position cannot fill the order
12737        cache
12738            .borrow_mut()
12739            .add_pool_profiler(test_profiler_with_range(
12740                &pool,
12741                FIXTURE_BLOCK,
12742                FIXTURE_BLOCK_HASH,
12743                U160::from(1u128 << 96),
12744                -10,
12745                10,
12746                1,
12747            ))
12748            .unwrap();
12749        let core = ExecutionClientCore::new(
12750            TraderId::from("TRADER-001"),
12751            ClientId::from("BLOCKCHAIN-001"),
12752            *BLOCKCHAIN_VENUE,
12753            OmsType::Netting,
12754            AccountId::from("BLOCKCHAIN-001"),
12755            AccountType::Wallet,
12756            None,
12757            cache,
12758        );
12759        let mut client =
12760            BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12761                .unwrap();
12762        let mut receiver = start_with_events(&mut client);
12763
12764        client.submit_order(submit_order_cmd(&order)).unwrap();
12765
12766        let events = collect_order_events(&mut receiver);
12767        assert_eq!(events.len(), 1);
12768        let OrderEventAny::Denied(denied) = &events[0] else {
12769            panic!("expected OrderDenied, was {:?}", events[0]);
12770        };
12771        assert!(
12772            denied.reason.as_str().contains("cannot fill the order"),
12773            "was: {}",
12774            denied.reason
12775        );
12776    }
12777
12778    #[tokio::test]
12779    async fn submit_order_applies_slippage_param_override_at_ceiling() {
12780        let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
12781            "execution_submit_slippage_override_test",
12782            swap_rpc_state_with_min_amount_out(expected_min_amount_out(200)).await,
12783        )
12784        .await
12785        else {
12786            return;
12787        };
12788        let order = test_market_sell_order(test_pool().instrument_id);
12789        let mut cmd = submit_order_cmd(&order);
12790        // Exactly at the configured ceiling (200 bps): accepted, not denied
12791        cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
12792        let mut receiver = start_with_events(&mut client);
12793        let expected_min_out = expected_min_amount_out(200);
12794
12795        client.submit_order(cmd).unwrap();
12796        await_pending_tasks(&client).await;
12797
12798        let events = collect_order_events(&mut receiver);
12799        assert_swap_submitted_and_filled(&events);
12800
12801        let (_, expected_raw) = expected_swap_tx(expected_min_out).await;
12802        let broadcasts: Vec<_> = state
12803            .recorded_requests()
12804            .into_iter()
12805            .filter(|request| request["method"] == "eth_sendRawTransaction")
12806            .collect();
12807        assert_eq!(broadcasts.len(), 1);
12808        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
12809
12810        drop_execution_schema(&admin_pool, &schema).await;
12811    }
12812
12813    #[tokio::test]
12814    async fn submit_order_accepts_quote_fresh_at_max_age_boundary() {
12815        let min_amount_out = expected_min_amount_out(50);
12816        let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
12817        let head = finalized_swap_block(tx_hash, min_amount_out);
12818        let state = swap_rpc_state()
12819            .await
12820            .with_response("eth_getBlockByNumber", &head)
12821            .with_parameter_response("eth_getBlockByNumber", FIXTURE_BLOCK_PARAM, BLOCK_BY_NUMBER);
12822        let Some((admin_pool, schema, mut client, state, cache)) =
12823            swap_client_with_database_config(
12824                "execution_submit_fresh_boundary_test",
12825                state,
12826                |http_rpc_url| {
12827                    let mut config = test_config(http_rpc_url);
12828                    config.max_quote_age_blocks = Some(1);
12829                    config
12830                },
12831            )
12832            .await
12833        else {
12834            return;
12835        };
12836        let pool = test_pool();
12837        cache
12838            .borrow_mut()
12839            .add_pool_profiler(test_profiler_at_block(
12840                &pool,
12841                FIXTURE_BLOCK,
12842                FIXTURE_BLOCK_HASH,
12843            ))
12844            .unwrap();
12845        let order = test_market_sell_order(pool.instrument_id);
12846        let mut receiver = start_with_events(&mut client);
12847
12848        client.submit_order(submit_order_cmd(&order)).unwrap();
12849        await_pending_tasks(&client).await;
12850
12851        let events = collect_order_events(&mut receiver);
12852        assert_eq!(events.len(), 1, "was: {events:?}");
12853        assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
12854        assert_eq!(
12855            state
12856                .recorded_requests()
12857                .iter()
12858                .filter(|request| request["method"] == "eth_sendRawTransaction")
12859                .count(),
12860            1
12861        );
12862
12863        drop_execution_schema(&admin_pool, &schema).await;
12864    }
12865
12866    #[tokio::test]
12867    async fn submit_order_sells_base_token_from_token1_position() {
12868        // A pool with the token order reversed relative to the convention: USDC is token0
12869        // and WETH is token1, so the base token (WETH, by token priority) sits in the
12870        // token1 position and the swap quotes zero_for_one = false
12871        let chain = Arc::new(chains::ARBITRUM.clone());
12872        let dex = UNISWAP_V3.dex.clone();
12873        let usdc = Token::new(
12874            chain.clone(),
12875            address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
12876            "USD Coin".to_string(),
12877            "USDC".to_string(),
12878            6,
12879        );
12880        let weth = Token::new(
12881            chain.clone(),
12882            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
12883            "Wrapped Ether".to_string(),
12884            "WETH".to_string(),
12885            18,
12886        );
12887        let pool = Pool::new(
12888            chain,
12889            dex,
12890            address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
12891            PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
12892            55_000_000,
12893            usdc,
12894            weth,
12895            Some(500),
12896            Some(10),
12897            UnixNanos::default(),
12898        );
12899
12900        let cache = Rc::new(RefCell::new(Cache::default()));
12901        cache.borrow_mut().add_pool(pool.clone()).unwrap();
12902        let order = test_market_sell_order(pool.instrument_id);
12903        cache
12904            .borrow_mut()
12905            .add_order(order.clone(), None, None, false)
12906            .unwrap();
12907        // Asymmetric price so the zero_for_one direction changes the quote
12908        cache
12909            .borrow_mut()
12910            .add_pool_profiler(test_profiler_with_state(
12911                &pool,
12912                FIXTURE_BLOCK,
12913                U160::from(2u128 << 96),
12914                TEST_LIQUIDITY,
12915            ))
12916            .unwrap();
12917
12918        let (admin_pool, pg_config) = match connect_test_postgres("orientation").await {
12919            Some(setup) => setup,
12920            None => return,
12921        };
12922        let schema = format!("execution_submit_orientation_test_{}", std::process::id());
12923        setup_execution_schema(&admin_pool, &schema).await;
12924        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12925        let db_options = db_options.options([("search_path", schema.clone())]);
12926        let database = connect_test_database(db_options).await.unwrap();
12927
12928        let state = swap_rpc_state()
12929            .await
12930            .with_call_response(POOL_TOKEN0_SELECTOR, CALL_USDC)
12931            .with_call_response(POOL_TOKEN1_SELECTOR, CALL_WETH)
12932            .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
12933        let addr = start_mock_rpc_server(state.clone()).await;
12934        let core = ExecutionClientCore::new(
12935            TraderId::from("TRADER-001"),
12936            ClientId::from("BLOCKCHAIN-001"),
12937            *BLOCKCHAIN_VENUE,
12938            OmsType::Netting,
12939            AccountId::from("BLOCKCHAIN-001"),
12940            AccountType::Wallet,
12941            None,
12942            cache,
12943        );
12944        let mut config = test_config(format!("http://{addr}"));
12945        let result = |response: &str| {
12946            serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
12947                .as_str()
12948                .unwrap()
12949                .to_string()
12950        };
12951        {
12952            let verification = config.verification.as_mut().unwrap();
12953            let pool_identity = &mut verification.deployment_manifest.pools[0];
12954            pool_identity.token0 = USDC.to_string();
12955            pool_identity.token1 = WETH.to_string();
12956            let pool_contract = verification
12957                .deployment_manifest
12958                .contracts
12959                .iter_mut()
12960                .find(|contract| contract.role == BlockchainContractRole::Pool)
12961                .unwrap();
12962
12963            for probe in &mut pool_contract.probes {
12964                if probe.call_data.starts_with(POOL_TOKEN0_SELECTOR) {
12965                    probe.expected_output = result(CALL_USDC);
12966                } else if probe.call_data.starts_with(POOL_TOKEN1_SELECTOR) {
12967                    probe.expected_output = result(CALL_WETH);
12968                }
12969            }
12970        }
12971        refresh_test_manifest_digest(&mut config);
12972        let mut client = BlockchainExecutionClient::new(core, config).unwrap();
12973        client.cache.database = Some(database);
12974        client
12975            .cache
12976            .ensure_execution_transaction_schema()
12977            .await
12978            .unwrap();
12979        protect_test_storage(&mut client, &schema).await;
12980        initialize_test_verification_ledger(&client).await;
12981        client.signer = Some(Arc::new(
12982            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
12983        ));
12984        client.core.set_connected();
12985        let mut receiver = start_with_events(&mut client);
12986
12987        let profiler = test_profiler_with_state(
12988            &pool,
12989            FIXTURE_BLOCK,
12990            U160::from(2u128 << 96),
12991            TEST_LIQUIDITY,
12992        );
12993        let quote = profiler
12994            .swap_exact_in(U256::from(1_000_000_000_000_000u64), false, None)
12995            .unwrap();
12996        let quoted = exact_output_amount(&quote, false).unwrap();
12997        let expected_min_out = derive_min_amount_out(quoted, 50).unwrap();
12998        let wrong_direction_quote = profiler
12999            .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
13000            .unwrap();
13001        let wrong_direction_out = exact_output_amount(&wrong_direction_quote, true).unwrap();
13002        assert_ne!(
13003            quoted, wrong_direction_out,
13004            "the asymmetric price must make the quote direction observable"
13005        );
13006        assert_ne!(
13007            expected_min_out,
13008            expected_min_amount_out(50),
13009            "the profiler quote must remain distinct from the independent quote fixture"
13010        );
13011
13012        client.submit_order(submit_order_cmd(&order)).unwrap();
13013        await_pending_tasks(&client).await;
13014
13015        let events = collect_order_events(&mut receiver);
13016        assert_eq!(events.len(), 1);
13017        assert!(
13018            matches!(&events[0], OrderEventAny::Submitted(_)),
13019            "was: {:?}",
13020            events[0]
13021        );
13022
13023        let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
13024        let broadcasts: Vec<_> = state
13025            .recorded_requests()
13026            .into_iter()
13027            .filter(|request| request["method"] == "eth_sendRawTransaction")
13028            .collect();
13029        assert_eq!(broadcasts.len(), 1);
13030        assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
13031
13032        drop_execution_schema(&admin_pool, &schema).await;
13033    }
13034
13035    #[tokio::test]
13036    async fn submit_order_denies_on_chain_mismatch() {
13037        let state = swap_rpc_state()
13038            .await
13039            .with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13040        let Some((admin_pool, schema, mut client, state, _)) =
13041            swap_client_with_database("execution_submit_chain_mismatch_test", state).await
13042        else {
13043            return;
13044        };
13045        let order = test_market_sell_order(test_pool().instrument_id);
13046        let mut receiver = start_with_events(&mut client);
13047
13048        client.submit_order(submit_order_cmd(&order)).unwrap();
13049        await_pending_tasks(&client).await;
13050
13051        let events = collect_order_events(&mut receiver);
13052        assert_eq!(events.len(), 1);
13053        let OrderEventAny::Denied(denied) = &events[0] else {
13054            panic!("expected OrderDenied, was {:?}", events[0]);
13055        };
13056        assert!(
13057            denied
13058                .reason
13059                .as_str()
13060                .contains("pre-sign chain ID verification disagreed"),
13061            "was: {}",
13062            denied.reason
13063        );
13064        let requests = state.recorded_requests();
13065        assert!(
13066            requests
13067                .iter()
13068                .all(|request| request["method"] != "eth_sendRawTransaction"),
13069            "no broadcast may follow a chain mismatch"
13070        );
13071        assert!(client.in_flight.lock().is_none());
13072
13073        drop_execution_schema(&admin_pool, &schema).await;
13074    }
13075
13076    #[tokio::test]
13077    async fn submit_order_finality_timeout_marks_dropped_and_keeps_ownership() {
13078        let receipt_release = Arc::new(tokio::sync::Semaphore::new(0));
13079        let state = swap_rpc_state()
13080            .await
13081            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
13082            .with_response_release("eth_getTransactionReceipt", Arc::clone(&receipt_release));
13083        let Some((admin_pool, schema, mut client, state, _)) =
13084            swap_client_with_database("execution_submit_inclusion_timeout_test", state).await
13085        else {
13086            return;
13087        };
13088        client.transaction_limits.receipt_timeout_secs = 1;
13089        let order = test_market_sell_order(test_pool().instrument_id);
13090        let mut receiver = start_with_events(&mut client);
13091
13092        client.submit_order(submit_order_cmd(&order)).unwrap();
13093        await_recorded_requests(&state, "eth_getTransactionReceipt", 3).await;
13094        tokio::time::timeout(Duration::from_secs(3), await_pending_tasks(&client))
13095            .await
13096            .unwrap();
13097        receipt_release.add_permits(3);
13098
13099        // Broadcast acceptance was observed, so the order is submitted; the unobserved
13100        // inclusion justifies no further event
13101        let events = collect_order_events(&mut receiver);
13102        assert_eq!(events.len(), 1);
13103        assert!(
13104            matches!(&events[0], OrderEventAny::Submitted(_)),
13105            "was: {:?}",
13106            events[0]
13107        );
13108
13109        let record = client
13110            .cache
13111            .get_execution_transaction(
13112                42161,
13113                &expected_swap_tx(expected_min_amount_out(50))
13114                    .await
13115                    .0
13116                    .to_string(),
13117            )
13118            .await
13119            .unwrap()
13120            .unwrap();
13121        assert_eq!(record.status, "dropped");
13122        assert!(client.in_flight.lock().is_some());
13123
13124        drop_execution_schema(&admin_pool, &schema).await;
13125    }
13126
13127    #[tokio::test]
13128    async fn included_receipt_finality_timeout_marks_dropped_and_keeps_ownership() {
13129        let state = execution_rpc_state()
13130            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
13131            .with_response("eth_estimateGas", ESTIMATE_GAS)
13132            .with_response("eth_call", CALL_BALANCE)
13133            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
13134            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
13135            .with_send_raw_transaction_echo();
13136        let Some((admin_pool, schema, mut client, _)) =
13137            execution_client_with_database("execution_included_timeout_test", state).await
13138        else {
13139            return;
13140        };
13141        client.transaction_limits.receipt_timeout_secs = 1;
13142
13143        let error = client
13144            .wrap(U256::from(1_000_000_000_000_000_u64))
13145            .await
13146            .unwrap_err();
13147        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13148            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
13149        )))
13150        .fetch_all(&admin_pool)
13151        .await
13152        .unwrap();
13153
13154        assert!(
13155            error.to_string().contains("Timed out awaiting finality"),
13156            "was: {error}"
13157        );
13158        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
13159        assert!(client.in_flight.lock().is_some());
13160
13161        drop_execution_schema(&admin_pool, &schema).await;
13162    }
13163
13164    #[tokio::test]
13165    async fn submit_order_single_in_flight_rejects_concurrent_swap() {
13166        let broadcast_release = Arc::new(tokio::sync::Semaphore::new(0));
13167        let state = swap_rpc_state()
13168            .await
13169            .with_response_release("eth_sendRawTransaction", Arc::clone(&broadcast_release));
13170        let Some((admin_pool, schema, mut client, state, cache)) =
13171            swap_client_with_database("execution_submit_concurrent_test", state).await
13172        else {
13173            return;
13174        };
13175        let pool = test_pool();
13176        let first = test_market_sell_order(pool.instrument_id);
13177        let second = OrderTestBuilder::new(OrderType::Market)
13178            .trader_id(TraderId::from("TRADER-001"))
13179            .strategy_id(StrategyId::from("S-001"))
13180            .instrument_id(pool.instrument_id)
13181            .client_order_id(ClientOrderId::from("O-SWAP-002"))
13182            .side(OrderSide::Sell)
13183            .quantity(Quantity::from("0.001"))
13184            .build();
13185        cache
13186            .borrow_mut()
13187            .add_order(second.clone(), None, None, false)
13188            .unwrap();
13189        let mut receiver = start_with_events(&mut client);
13190
13191        client.submit_order(submit_order_cmd(&first)).unwrap();
13192        await_recorded_requests(&state, "eth_sendRawTransaction", 1).await;
13193        client.submit_order(submit_order_cmd(&second)).unwrap();
13194        let event = tokio::time::timeout(TEST_TIMEOUT, receiver.recv())
13195            .await
13196            .unwrap()
13197            .unwrap();
13198        let ExecutionEvent::Order(OrderEventAny::Denied(denied)) = event else {
13199            panic!("expected OrderDenied, was {event:?}");
13200        };
13201        assert_eq!(denied.client_order_id, second.client_order_id());
13202        assert!(
13203            denied
13204                .reason
13205                .as_str()
13206                .contains("at most one transaction can be in flight"),
13207            "was: {}",
13208            denied.reason
13209        );
13210        broadcast_release.add_permits(1);
13211        await_pending_tasks(&client).await;
13212
13213        let events = collect_order_events(&mut receiver);
13214        assert_swap_submitted_and_filled(&events);
13215
13216        let broadcasts = state
13217            .recorded_requests()
13218            .into_iter()
13219            .filter(|request| request["method"] == "eth_sendRawTransaction")
13220            .count();
13221        assert_eq!(broadcasts, 1);
13222        let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13223            "SELECT COUNT(*) FROM {schema}.execution_intent"
13224        )))
13225        .fetch_one(&admin_pool)
13226        .await
13227        .unwrap();
13228        assert_eq!(row_count, 1);
13229        assert!(client.in_flight.lock().is_none());
13230
13231        drop_execution_schema(&admin_pool, &schema).await;
13232    }
13233
13234    #[tokio::test]
13235    async fn preflight_ready_when_all_checks_pass() {
13236        let (client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13237        let pool = test_pool();
13238
13239        let report = client.preflight(&pool.instrument_id).await.unwrap();
13240
13241        assert!(report.ready, "issues: {:?}", report.issues);
13242        assert!(report.issues.is_empty());
13243        assert_eq!(report.expected_chain_id, 42161);
13244        assert_eq!(report.actual_chain_id, 42161);
13245        assert!(report.chain_id_matches);
13246        assert_eq!(
13247            report.pool.address,
13248            address!("C6962004f452bE9203591991D15f6b388e09E8D0")
13249        );
13250        assert!(report.pool.has_deployed_code);
13251        assert_eq!(report.pool.fee, Some(500));
13252        assert_eq!(
13253            report.pool.base_token,
13254            address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1")
13255        );
13256        assert_eq!(
13257            report.pool.quote_token,
13258            address!("af88d065e77c8cC2239327C5EDb3A432268e5831")
13259        );
13260        assert_eq!(report.routers.len(), 1);
13261        assert!(report.routers[0].has_deployed_code);
13262        assert_eq!(report.tokens.len(), 2);
13263        assert_eq!(
13264            report.tokens[0].wallet_balance,
13265            U256::from(500_000_000_000_000_000u64)
13266        );
13267        assert_eq!(
13268            report.tokens[0].router_allowances,
13269            vec![(
13270                address!("E592427A0AEce92De3Edee1F18E0157C05861564"),
13271                U256::from(1_000_000_000_000_000_000u64)
13272            )]
13273        );
13274        assert_eq!(
13275            report.native_balance_wei,
13276            U256::from(1_000_000_000_000_000_000u64)
13277        );
13278        assert_eq!(report.base_fee_per_gas_wei, 100_000_000);
13279        assert_eq!(report.max_priority_fee_per_gas_wei, 10_000_000);
13280        assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13281        assert!(report.fee_within_ceiling);
13282
13283        let requests = state.recorded_requests();
13284        for method in ["eth_getCode", "eth_call", "eth_getBalance"] {
13285            let matching: Vec<_> = requests
13286                .iter()
13287                .filter(|request| request["method"] == method)
13288                .collect();
13289            assert!(!matching.is_empty(), "method {method}");
13290            assert!(
13291                matching
13292                    .iter()
13293                    .all(|request| request["params"][1] == "latest"),
13294                "method {method}: {matching:?}"
13295            );
13296        }
13297    }
13298
13299    #[tokio::test]
13300    async fn preflight_not_ready_without_pool_fee() {
13301        let addr = start_mock_rpc_server(ready_rpc_state()).await;
13302        let mut pool = test_pool();
13303        pool.fee = None;
13304        let client = test_client_from_config(test_config(format!("http://{addr}")), pool.clone());
13305
13306        let report = client.preflight(&pool.instrument_id).await.unwrap();
13307
13308        assert!(!report.ready);
13309        assert_eq!(report.pool.fee, None);
13310        assert_eq!(report.issues, vec!["Pool fee tier is missing"]);
13311    }
13312
13313    #[tokio::test]
13314    async fn preflight_not_ready_on_wrong_chain() {
13315        let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13316        let (client, _) = client_with_mock_rpc(state).await;
13317        let pool = test_pool();
13318
13319        let report = client.preflight(&pool.instrument_id).await.unwrap();
13320
13321        assert!(!report.ready);
13322        assert!(!report.chain_id_matches);
13323        assert!(
13324            report
13325                .issues
13326                .iter()
13327                .any(|issue| issue.contains("Chain ID mismatch"))
13328        );
13329    }
13330
13331    #[tokio::test]
13332    async fn preflight_not_ready_without_deployed_code() {
13333        let state = ready_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13334        let (client, _) = client_with_mock_rpc(state).await;
13335        let pool = test_pool();
13336
13337        let report = client.preflight(&pool.instrument_id).await.unwrap();
13338
13339        assert!(!report.ready);
13340        assert!(!report.pool.has_deployed_code);
13341        assert!(!report.routers[0].has_deployed_code);
13342        assert!(report.tokens.iter().all(|t| !t.has_deployed_code));
13343        assert!(
13344            report
13345                .issues
13346                .iter()
13347                .any(|issue| issue.contains("No deployed bytecode at router address"))
13348        );
13349    }
13350
13351    #[tokio::test]
13352    async fn preflight_not_ready_with_zero_balances_and_allowance() {
13353        let state = ready_rpc_state()
13354            .with_response("eth_getBalance", GET_BALANCE_ZERO)
13355            .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO)
13356            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
13357        let (client, _) = client_with_mock_rpc(state).await;
13358        let pool = test_pool();
13359
13360        let report = client.preflight(&pool.instrument_id).await.unwrap();
13361
13362        assert!(!report.ready);
13363        assert!(
13364            report
13365                .issues
13366                .iter()
13367                .any(|issue| issue.contains("Native currency balance is zero"))
13368        );
13369        assert!(
13370            report
13371                .issues
13372                .iter()
13373                .any(|issue| issue.contains("balance is zero"))
13374        );
13375        assert!(
13376            report
13377                .issues
13378                .iter()
13379                .any(|issue| issue.contains("No router allowance"))
13380        );
13381    }
13382
13383    #[tokio::test]
13384    async fn preflight_not_ready_when_fees_exceed_ceiling() {
13385        let addr = start_mock_rpc_server(ready_rpc_state()).await;
13386        let mut config = test_config(format!("http://{addr}"));
13387        config.max_fee_per_gas_wei = 1;
13388        let pool = test_pool();
13389        let client = test_client_from_config(config, pool.clone());
13390
13391        let report = client.preflight(&pool.instrument_id).await.unwrap();
13392
13393        assert!(!report.ready);
13394        assert!(!report.fee_within_ceiling);
13395        assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13396        assert!(
13397            report
13398                .issues
13399                .iter()
13400                .any(|issue| issue.contains("exceeds ceiling"))
13401        );
13402    }
13403
13404    #[rstest]
13405    fn resolve_pool_rejects_unknown_pool() {
13406        let client = test_client("http://127.0.0.1:1".to_string());
13407        let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
13408            .parse()
13409            .unwrap();
13410
13411        let error = client.resolve_pool(&unknown).unwrap_err();
13412
13413        assert!(error.to_string().contains("Unknown pool"), "was: {error}");
13414    }
13415
13416    #[rstest]
13417    fn resolve_pool_rejects_mismatched_chain() {
13418        let client = test_client("http://127.0.0.1:1".to_string());
13419        let ethereum_pool: InstrumentId =
13420            "0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3"
13421                .parse()
13422                .unwrap();
13423
13424        let error = client.resolve_pool(&ethereum_pool).unwrap_err();
13425
13426        assert!(
13427            error
13428                .to_string()
13429                .contains("does not match the client chain"),
13430            "was: {error}"
13431        );
13432    }
13433
13434    #[rstest]
13435    fn resolve_pool_rejects_unsupported_dex() {
13436        let client = test_client("http://127.0.0.1:1".to_string());
13437        let v4_pool: InstrumentId = "0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4"
13438            .parse()
13439            .unwrap();
13440
13441        let error = client.resolve_pool(&v4_pool).unwrap_err();
13442
13443        assert!(
13444            error.to_string().contains("only UniswapV3 is supported"),
13445            "was: {error}"
13446        );
13447    }
13448
13449    #[rstest]
13450    fn resolve_pool_rejects_pool_id_identifier() {
13451        let client = test_client("http://127.0.0.1:1".to_string());
13452        let pool_id: InstrumentId =
13453            "0x0000000000000000000000000000000000000000000000000000000000000000.Arbitrum:UniswapV3"
13454                .parse()
13455                .unwrap();
13456
13457        let error = client.resolve_pool(&pool_id).unwrap_err();
13458
13459        assert!(
13460            error
13461                .to_string()
13462                .contains("only address identifiers are supported"),
13463            "was: {error}"
13464        );
13465    }
13466
13467    #[rstest]
13468    fn resolve_pool_rejects_ambiguous_token_priority() {
13469        let chain = Arc::new(chains::ARBITRUM.clone());
13470        let dex = UNISWAP_V3.dex.clone();
13471        let token_a = Token::new(
13472            chain.clone(),
13473            address!("1111111111111111111111111111111111111111"),
13474            "Token A".to_string(),
13475            "TOKA".to_string(),
13476            18,
13477        );
13478        let token_b = Token::new(
13479            chain.clone(),
13480            address!("2222222222222222222222222222222222222222"),
13481            "Token B".to_string(),
13482            "TOKB".to_string(),
13483            18,
13484        );
13485        let pool = Pool::new(
13486            chain,
13487            dex,
13488            address!("3333333333333333333333333333333333333333"),
13489            PoolIdentifier::from_address(address!("3333333333333333333333333333333333333333")),
13490            55_000_000,
13491            token_a,
13492            token_b,
13493            Some(500),
13494            Some(10),
13495            UnixNanos::default(),
13496        );
13497        let client =
13498            test_client_from_config(test_config("http://127.0.0.1:1".to_string()), pool.clone());
13499
13500        let error = client.resolve_pool(&pool.instrument_id).unwrap_err();
13501
13502        assert!(error.to_string().contains("ambiguous"), "was: {error}");
13503    }
13504
13505    #[rstest]
13506    fn new_parses_pair_specific_quote_spend_limits_with_distinct_precisions() {
13507        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13508        config.quote_spend_limits = Some(vec![
13509            quote_spend_limit(WETH, USDC, 18, &U256::MAX.to_string()),
13510            quote_spend_limit(USDC, WETH, 6, "1000000000"),
13511        ]);
13512
13513        let client = test_client_from_config(config, test_pool());
13514        let sell_ceiling = client
13515            .transaction_limits
13516            .quote_spend_limits
13517            .get(&(WETH_ADDRESS, USDC_ADDRESS))
13518            .unwrap();
13519        let buy_ceiling = client
13520            .transaction_limits
13521            .quote_spend_limits
13522            .get(&(USDC_ADDRESS, WETH_ADDRESS))
13523            .unwrap();
13524
13525        assert_eq!(sell_ceiling.spend_token, WETH_ADDRESS);
13526        assert_eq!(sell_ceiling.spend_token_decimals, 18);
13527        assert_eq!(sell_ceiling.max_amount, U256::MAX);
13528        assert_eq!(buy_ceiling.spend_token, USDC_ADDRESS);
13529        assert_eq!(buy_ceiling.spend_token_decimals, 6);
13530        assert_eq!(buy_ceiling.max_amount, U256::from(1_000_000_000u64));
13531    }
13532
13533    #[rstest]
13534    fn new_rejects_quote_spend_limit_token_pair_mismatch() {
13535        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13536        config.quote_spend_limits.as_mut().unwrap()[0].spend_token = WETH.to_string();
13537
13538        let error = test_client_result(config, test_pool()).unwrap_err();
13539
13540        assert!(
13541            error
13542                .to_string()
13543                .contains("`spend_token` must match `token_in`"),
13544            "was: {error}"
13545        );
13546    }
13547
13548    #[rstest]
13549    fn new_rejects_quote_spend_limit_pair_outside_allowlist() {
13550        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13551        config.quote_spend_limits = Some(vec![quote_spend_limit(
13552            USDC,
13553            "0x1111111111111111111111111111111111111111",
13554            6,
13555            "1000000000",
13556        )]);
13557
13558        let error = test_client_result(config, test_pool()).unwrap_err();
13559
13560        assert!(
13561            error
13562                .to_string()
13563                .contains("is not in the `allowed_token_pairs` allowlist"),
13564            "was: {error}"
13565        );
13566    }
13567
13568    #[rstest]
13569    fn new_rejects_duplicate_quote_spend_pairs() {
13570        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13571        config.quote_spend_limits = Some(vec![
13572            quote_spend_limit(USDC, WETH, 6, "1000000000"),
13573            quote_spend_limit(USDC, WETH, 6, "2000000000"),
13574        ]);
13575
13576        let error = test_client_result(config, test_pool()).unwrap_err();
13577
13578        assert!(
13579            error
13580                .to_string()
13581                .contains("Duplicate quote spend limit for token pair"),
13582            "was: {error}"
13583        );
13584    }
13585
13586    #[rstest]
13587    #[case::empty("")]
13588    #[case::signed("-1")]
13589    #[case::fractional("1.5")]
13590    #[case::hexadecimal("0x10")]
13591    #[case::overflow(
13592        "115792089237316195423570985008687907853269984665640564039457584007913129639936"
13593    )]
13594    fn new_rejects_invalid_quote_spend_max_amount(#[case] max_amount: &str) {
13595        let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13596        config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount.to_string();
13597
13598        let error = test_client_result(config, test_pool()).unwrap_err();
13599
13600        assert!(
13601            error.to_string().contains("Quote spend limit `max_amount`"),
13602            "was: {error}"
13603        );
13604    }
13605
13606    #[rstest]
13607    #[case::allowed_token_pairs("allowed_token_pairs")]
13608    #[case::slippage_bps("slippage_bps")]
13609    #[case::max_slippage_bps("max_slippage_bps")]
13610    #[case::max_order_amount("max_order_amount")]
13611    #[case::deadline_seconds("deadline_seconds")]
13612    #[case::max_quote_age_blocks("max_quote_age_blocks")]
13613    #[case::receipt_timeout_secs("receipt_timeout_secs")]
13614    fn new_rejects_each_missing_transaction_limit(#[case] missing: &str) {
13615        let mut config = test_config("http://127.0.0.1:1".to_string());
13616        match missing {
13617            "allowed_token_pairs" => config.allowed_token_pairs = None,
13618            "slippage_bps" => config.slippage_bps = None,
13619            "max_slippage_bps" => config.max_slippage_bps = None,
13620            "max_order_amount" => config.max_order_amount = None,
13621            "deadline_seconds" => config.deadline_seconds = None,
13622            "max_quote_age_blocks" => config.max_quote_age_blocks = None,
13623            "receipt_timeout_secs" => config.receipt_timeout_secs = None,
13624            _ => unreachable!(),
13625        }
13626
13627        let error = test_client_result(config, test_pool()).unwrap_err();
13628
13629        assert_eq!(
13630            error.to_string(),
13631            "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"
13632        );
13633    }
13634
13635    #[rstest]
13636    fn new_rejects_empty_router_allowlist() {
13637        let mut config = test_config("http://127.0.0.1:1".to_string());
13638        config.router_addresses = Vec::new();
13639
13640        let error = test_client_result(config, test_pool()).unwrap_err();
13641
13642        assert!(
13643            error.to_string().contains("at least one router address"),
13644            "was: {error}"
13645        );
13646    }
13647
13648    #[tokio::test]
13649    async fn wrap_refuses_without_durable_store() {
13650        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13651        client.core.set_connected();
13652
13653        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13654
13655        assert!(
13656            error.to_string().contains("No durable store configured"),
13657            "was: {error}"
13658        );
13659    }
13660
13661    #[tokio::test]
13662    async fn approve_rejects_router_outside_allowlist() {
13663        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13664
13665        let error = client
13666            .approve(
13667                WETH_ADDRESS,
13668                U256::from(1_000u64),
13669                address!("68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"),
13670            )
13671            .await
13672            .err()
13673            .unwrap();
13674
13675        assert!(
13676            error
13677                .to_string()
13678                .contains("not in the configured `router_addresses` allowlist"),
13679            "was: {error}"
13680        );
13681        assert!(state.recorded_requests().is_empty());
13682    }
13683
13684    #[tokio::test]
13685    async fn approve_rejects_token_outside_input_allowlist() {
13686        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13687
13688        let error = client
13689            .approve(USDC_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13690            .await
13691            .unwrap_err();
13692
13693        assert!(
13694            error
13695                .to_string()
13696                .contains("is not an input token in the configured `allowed_token_pairs`"),
13697            "was: {error}"
13698        );
13699        assert!(state.recorded_requests().is_empty());
13700    }
13701
13702    #[tokio::test]
13703    async fn in_flight_guard_rejects_second_transaction() {
13704        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13705        client.core.set_connected();
13706        *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
13707            intent_id: 1,
13708            nonce: 7,
13709            tx_hash: B256::ZERO,
13710            purpose: TransactionPurpose::Wrap,
13711        }));
13712
13713        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13714
13715        assert!(
13716            error.to_string().contains("still awaiting finality"),
13717            "was: {error}"
13718        );
13719    }
13720
13721    #[tokio::test]
13722    async fn wrap_rejects_zero_amount() {
13723        let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13724
13725        let error = client.wrap(U256::ZERO).await.unwrap_err();
13726
13727        assert!(
13728            error.to_string().contains("Wrap amount must be positive"),
13729            "was: {error}"
13730        );
13731    }
13732
13733    #[tokio::test]
13734    async fn wrap_rejects_code_free_target_before_broadcast() {
13735        let state = execution_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13736        let Some((admin_pool, schema, mut client, state)) =
13737            execution_client_with_database("execution_wrap_code_free_test", state).await
13738        else {
13739            return;
13740        };
13741
13742        let error = client
13743            .wrap(U256::from(1_000_000_000_000_000u64))
13744            .await
13745            .unwrap_err();
13746
13747        assert!(
13748            error
13749                .to_string()
13750                .contains("pre-sign deployment manifest verification disagreed"),
13751            "was: {error}"
13752        );
13753        assert!(client.in_flight.lock().is_none());
13754        let requests = state.recorded_requests();
13755        assert_eq!(
13756            requests
13757                .iter()
13758                .filter(|request| request["method"] == "eth_getCode")
13759                .count(),
13760            3
13761        );
13762        assert!(
13763            requests
13764                .iter()
13765                .all(|request| request["method"] != "eth_sendRawTransaction")
13766        );
13767
13768        drop_execution_schema(&admin_pool, &schema).await;
13769    }
13770
13771    #[tokio::test]
13772    async fn wrap_rejects_unrelated_target_before_broadcast() {
13773        let state = execution_rpc_state().with_response("eth_call", CALL_EMPTY);
13774        let Some((admin_pool, schema, mut client, state)) =
13775            execution_client_with_database("execution_wrap_unrelated_test", state).await
13776        else {
13777            return;
13778        };
13779
13780        let error = client
13781            .wrap(U256::from(1_000_000_000_000_000u64))
13782            .await
13783            .unwrap_err();
13784
13785        assert!(
13786            error
13787                .to_string()
13788                .contains("pre-sign wrapped token probe verification is unavailable"),
13789            "was: {error}"
13790        );
13791        assert!(client.in_flight.lock().is_none());
13792        let requests = state.recorded_requests();
13793        assert!(
13794            requests
13795                .iter()
13796                .all(|request| request["method"] != "eth_sendRawTransaction")
13797        );
13798
13799        drop_execution_schema(&admin_pool, &schema).await;
13800    }
13801
13802    #[tokio::test]
13803    async fn wrap_rejects_included_transaction_without_balance_delta() {
13804        let state = broadcast_rpc_state().with_response_sequence("eth_call", &[CALL_BALANCE; 9]);
13805        let Some((admin_pool, schema, mut client, state)) =
13806            execution_client_with_database("execution_wrap_no_delta_test", state).await
13807        else {
13808            return;
13809        };
13810
13811        let error = client
13812            .wrap(U256::from(1_000_000_000_000_000u64))
13813            .await
13814            .unwrap_err();
13815
13816        assert!(
13817            error.to_string().contains("did not increase"),
13818            "was: {error}"
13819        );
13820        let in_flight = awaiting_in_flight(&client);
13821        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13822        assert_eq!(
13823            execution_intent_markers(&admin_pool, &schema).await,
13824            vec![("wrap".into(), "broadcast".into(), false, true)]
13825        );
13826        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13827            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13828        )))
13829        .fetch_one(&admin_pool)
13830        .await
13831        .unwrap();
13832        assert_eq!(nonce_state, (7, 0));
13833        let broadcasts = state
13834            .recorded_requests()
13835            .into_iter()
13836            .filter(|request| request["method"] == "eth_sendRawTransaction")
13837            .count();
13838        assert_eq!(broadcasts, 1);
13839
13840        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
13841        let block = finalized_wrap_block(expected_hash);
13842        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
13843        let restart_state = with_finalized_identity(
13844            execution_rpc_state()
13845                .with_response("eth_getTransactionReceipt", &receipt)
13846                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
13847                .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
13848            &block,
13849            &receipt,
13850        );
13851        let addr = start_mock_rpc_server(restart_state).await;
13852        let error = later_reconnect(client, format!("http://{addr}")).await;
13853        assert!(
13854            error.to_string().contains("did not increase"),
13855            "was: {error}"
13856        );
13857        assert_eq!(
13858            execution_intent_markers(&admin_pool, &schema).await,
13859            vec![("wrap".into(), "broadcast".into(), false, true)]
13860        );
13861        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13862            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13863        )))
13864        .fetch_one(&admin_pool)
13865        .await
13866        .unwrap();
13867        assert_eq!(nonce_state, (7, 0));
13868
13869        drop_execution_schema(&admin_pool, &schema).await;
13870    }
13871
13872    #[tokio::test]
13873    async fn wrap_reports_inclusion_when_postcondition_read_fails() {
13874        let state = broadcast_rpc_state().with_response_sequence(
13875            "eth_call",
13876            &[
13877                CALL_BALANCE,
13878                CALL_BALANCE,
13879                CALL_BALANCE,
13880                CALL_BALANCE,
13881                CALL_BALANCE,
13882                CALL_BALANCE,
13883                RPC_METHOD_NOT_FOUND,
13884                RPC_METHOD_NOT_FOUND,
13885                RPC_METHOD_NOT_FOUND,
13886            ],
13887        );
13888        let Some((admin_pool, schema, mut client, _)) =
13889            execution_client_with_database("execution_wrap_postcondition_rpc_test", state).await
13890        else {
13891            return;
13892        };
13893
13894        let error = client
13895            .wrap(U256::from(1_000_000_000_000_000u64))
13896            .await
13897            .unwrap_err();
13898
13899        let message = error.to_string();
13900        assert!(
13901            message.contains("failed to verify WETH balance after included transaction 0x"),
13902            "was: {message}"
13903        );
13904        assert!(message.contains("at block 30346561"), "was: {message}");
13905        let in_flight = awaiting_in_flight(&client);
13906        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13907        assert_eq!(
13908            execution_intent_markers(&admin_pool, &schema).await,
13909            vec![("wrap".into(), "broadcast".into(), false, true)]
13910        );
13911
13912        drop_execution_schema(&admin_pool, &schema).await;
13913    }
13914
13915    #[tokio::test]
13916    async fn approve_rejects_false_return_before_broadcast() {
13917        let state = execution_rpc_state().with_response("eth_call", CALL_ZERO);
13918        let Some((admin_pool, schema, mut client, state)) =
13919            execution_client_with_database("execution_approve_false_test", state).await
13920        else {
13921            return;
13922        };
13923
13924        let error = client
13925            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13926            .await
13927            .unwrap_err();
13928
13929        assert!(error.to_string().contains("returned false"), "was: {error}");
13930        assert!(client.in_flight.lock().is_none());
13931        let requests = state.recorded_requests();
13932        let approval_calls = requests
13933            .iter()
13934            .filter(|request| {
13935                request["method"] == "eth_call"
13936                    && request["params"][0]["data"]
13937                        .as_str()
13938                        .is_some_and(|data| data.starts_with("0x095ea7b3"))
13939            })
13940            .collect::<Vec<_>>();
13941        assert_eq!(approval_calls.len(), 3);
13942        for request in approval_calls {
13943            assert_eq!(
13944                request["params"][0]["from"]
13945                    .as_str()
13946                    .unwrap()
13947                    .parse::<Address>()
13948                    .unwrap(),
13949                WALLET.parse::<Address>().unwrap()
13950            );
13951            assert_eq!(request["params"][1], FIXTURE_BLOCK_PARAM);
13952        }
13953        assert!(
13954            requests
13955                .iter()
13956                .all(|request| request["method"] != "eth_getTransactionCount")
13957        );
13958        assert!(
13959            requests
13960                .iter()
13961                .all(|request| request["method"] != "eth_sendRawTransaction")
13962        );
13963
13964        drop_execution_schema(&admin_pool, &schema).await;
13965    }
13966
13967    #[tokio::test]
13968    async fn approve_rejects_router_with_wrong_factory_before_signing() {
13969        let state = ready_rpc_state().with_call_response(FACTORY_SELECTOR, CALL_ZERO);
13970        let Some((admin_pool, schema, mut client, state)) =
13971            execution_client_with_database("execution_approve_factory_test", state).await
13972        else {
13973            return;
13974        };
13975
13976        let error = client
13977            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13978            .await
13979            .unwrap_err();
13980
13981        assert!(
13982            error
13983                .to_string()
13984                .contains("pre-sign deployment manifest verification disagreed"),
13985            "was: {error}"
13986        );
13987        let requests = state.recorded_requests();
13988        assert!(
13989            requests
13990                .iter()
13991                .all(|request| request["method"] != "eth_getTransactionCount")
13992        );
13993        assert!(
13994            requests
13995                .iter()
13996                .all(|request| request["method"] != "eth_sendRawTransaction")
13997        );
13998
13999        drop_execution_schema(&admin_pool, &schema).await;
14000    }
14001
14002    #[tokio::test]
14003    async fn approve_rejects_router_with_wrong_weth_before_signing() {
14004        let state = ready_rpc_state().with_call_response(WETH9_SELECTOR, CALL_ZERO);
14005        let Some((admin_pool, schema, mut client, state)) =
14006            execution_client_with_database("execution_approve_weth_test", state).await
14007        else {
14008            return;
14009        };
14010
14011        let error = client
14012            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14013            .await
14014            .unwrap_err();
14015
14016        assert!(
14017            error
14018                .to_string()
14019                .contains("pre-sign deployment manifest verification disagreed"),
14020            "was: {error}"
14021        );
14022        let requests = state.recorded_requests();
14023        assert!(
14024            requests
14025                .iter()
14026                .all(|request| request["method"] != "eth_getTransactionCount")
14027        );
14028        assert!(
14029            requests
14030                .iter()
14031                .all(|request| request["method"] != "eth_sendRawTransaction")
14032        );
14033
14034        drop_execution_schema(&admin_pool, &schema).await;
14035    }
14036
14037    #[tokio::test]
14038    async fn approve_rejects_nonzero_to_nonzero_transition_before_signing() {
14039        let state = ready_rpc_state();
14040        let Some((admin_pool, schema, mut client, state)) =
14041            execution_client_with_database("execution_approve_nonzero_test", state).await
14042        else {
14043            return;
14044        };
14045
14046        let error = client
14047            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14048            .await
14049            .unwrap_err();
14050
14051        assert!(
14052            error.to_string().contains("approve zero before setting"),
14053            "was: {error}"
14054        );
14055        let requests = state.recorded_requests();
14056        assert!(
14057            requests
14058                .iter()
14059                .all(|request| request["method"] != "eth_getTransactionCount")
14060        );
14061        assert!(
14062            requests
14063                .iter()
14064                .all(|request| request["method"] != "eth_sendRawTransaction")
14065        );
14066
14067        drop_execution_schema(&admin_pool, &schema).await;
14068    }
14069
14070    #[tokio::test]
14071    async fn approve_zero_revokes_under_unlimited_policy() {
14072        let state = broadcast_rpc_state()
14073            .with_response("eth_call", CALL_BOOL_TRUE)
14074            .with_call_response_sequence(
14075                ALLOWANCE_SELECTOR,
14076                &[
14077                    CALL_ALLOWANCE,
14078                    CALL_ALLOWANCE,
14079                    CALL_ALLOWANCE,
14080                    CALL_ZERO,
14081                    CALL_ZERO,
14082                    CALL_ZERO,
14083                ],
14084            );
14085        let Some((admin_pool, schema, mut client, state)) =
14086            execution_client_with_database("execution_approve_revoke_test", state).await
14087        else {
14088            return;
14089        };
14090        client.config.unlimited_approval = true;
14091
14092        let tx_hash = client
14093            .approve(WETH_ADDRESS, U256::ZERO, ROUTER_ADDRESS)
14094            .await
14095            .unwrap();
14096
14097        assert_eq!(tx_hash, expected_approve_tx_hash(U256::ZERO).await);
14098        let approve_data = state
14099            .recorded_requests()
14100            .into_iter()
14101            .find_map(|request| {
14102                (request["method"] == "eth_estimateGas")
14103                    .then(|| request["params"][0]["data"].as_str().map(str::to_owned))
14104                    .flatten()
14105            })
14106            .unwrap();
14107        assert!(approve_data.starts_with("0x095ea7b3"));
14108        assert!(approve_data.ends_with(&"0".repeat(64)));
14109        assert_eq!(
14110            state
14111                .recorded_requests()
14112                .iter()
14113                .filter(|request| {
14114                    request["method"] == "eth_call"
14115                        && request["params"][0]["data"]
14116                            .as_str()
14117                            .is_some_and(|data| data.starts_with(FACTORY_SELECTOR))
14118                })
14119                .count(),
14120            12
14121        );
14122        assert_eq!(
14123            execution_intent_markers(&admin_pool, &schema).await,
14124            vec![("approve".into(), "finalized".into(), true, false)]
14125        );
14126
14127        drop_execution_schema(&admin_pool, &schema).await;
14128    }
14129
14130    #[tokio::test]
14131    async fn approve_accepts_empty_return_with_sufficient_allowance() {
14132        let state = broadcast_rpc_state()
14133            .with_response("eth_call", CALL_EMPTY)
14134            .with_call_response_sequence(
14135                ALLOWANCE_SELECTOR,
14136                &[
14137                    CALL_ZERO,
14138                    CALL_ZERO,
14139                    CALL_ZERO,
14140                    CALL_ALLOWANCE_1000,
14141                    CALL_ALLOWANCE_1000,
14142                    CALL_ALLOWANCE_1000,
14143                ],
14144            );
14145        let Some((admin_pool, schema, mut client, _)) =
14146            execution_client_with_database("execution_approve_empty_test", state).await
14147        else {
14148            return;
14149        };
14150
14151        let tx_hash = client
14152            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14153            .await
14154            .unwrap();
14155
14156        let record = client
14157            .cache
14158            .get_execution_transaction(42161, &tx_hash.to_string())
14159            .await
14160            .unwrap()
14161            .unwrap();
14162        assert_eq!(record.purpose, "approve");
14163        assert_eq!(record.status, "finalized");
14164        assert!(client.in_flight.lock().is_none());
14165        assert_eq!(
14166            execution_intent_markers(&admin_pool, &schema).await,
14167            vec![("approve".into(), "finalized".into(), true, false)]
14168        );
14169
14170        drop_execution_schema(&admin_pool, &schema).await;
14171    }
14172
14173    #[tokio::test]
14174    async fn approve_rejects_empty_return_with_insufficient_allowance() {
14175        let state = broadcast_rpc_state()
14176            .with_response("eth_call", CALL_EMPTY)
14177            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 6]);
14178        let Some((admin_pool, schema, mut client, _)) =
14179            execution_client_with_database("execution_approve_insufficient_test", state).await
14180        else {
14181            return;
14182        };
14183
14184        let error = client
14185            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14186            .await
14187            .unwrap_err();
14188
14189        assert!(
14190            error
14191                .to_string()
14192                .contains("does not equal the requested amount"),
14193            "was: {error}"
14194        );
14195        let in_flight = awaiting_in_flight(&client);
14196        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14197        assert_eq!(
14198            execution_intent_markers(&admin_pool, &schema).await,
14199            vec![("approve".into(), "broadcast".into(), false, true)]
14200        );
14201        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14202            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14203        )))
14204        .fetch_one(&admin_pool)
14205        .await
14206        .unwrap();
14207        assert_eq!(nonce_state, (7, 0));
14208
14209        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
14210        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
14211        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
14212        let restart_state = with_finalized_identity(
14213            execution_rpc_state()
14214                .with_response("eth_getTransactionReceipt", &receipt)
14215                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
14216                .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
14217            &block,
14218            &receipt,
14219        );
14220        let addr = start_mock_rpc_server(restart_state).await;
14221        let error = later_reconnect(client, format!("http://{addr}")).await;
14222        assert!(
14223            error
14224                .to_string()
14225                .contains("does not equal the requested amount"),
14226            "was: {error}"
14227        );
14228        assert_eq!(
14229            execution_intent_markers(&admin_pool, &schema).await,
14230            vec![("approve".into(), "broadcast".into(), false, true)]
14231        );
14232        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14233            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14234        )))
14235        .fetch_one(&admin_pool)
14236        .await
14237        .unwrap();
14238        assert_eq!(nonce_state, (7, 0));
14239
14240        drop_execution_schema(&admin_pool, &schema).await;
14241    }
14242
14243    #[tokio::test]
14244    async fn approve_reports_inclusion_when_postcondition_read_fails() {
14245        let state = broadcast_rpc_state()
14246            .with_response("eth_call", CALL_BOOL_TRUE)
14247            .with_call_response_sequence(
14248                ALLOWANCE_SELECTOR,
14249                &[
14250                    CALL_ZERO,
14251                    CALL_ZERO,
14252                    CALL_ZERO,
14253                    RPC_METHOD_NOT_FOUND,
14254                    RPC_METHOD_NOT_FOUND,
14255                    RPC_METHOD_NOT_FOUND,
14256                ],
14257            );
14258        let Some((admin_pool, schema, mut client, _)) =
14259            execution_client_with_database("execution_approve_postcondition_rpc_test", state).await
14260        else {
14261            return;
14262        };
14263
14264        let error = client
14265            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14266            .await
14267            .unwrap_err();
14268
14269        let message = error.to_string();
14270        assert!(
14271            message.contains("failed to verify router allowance after included transaction 0x"),
14272            "was: {message}"
14273        );
14274        assert!(message.contains("at block 30346561"), "was: {message}");
14275        let in_flight = awaiting_in_flight(&client);
14276        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14277        assert_eq!(
14278            execution_intent_markers(&admin_pool, &schema).await,
14279            vec![("approve".into(), "broadcast".into(), false, true)]
14280        );
14281
14282        drop_execution_schema(&admin_pool, &schema).await;
14283    }
14284
14285    #[tokio::test]
14286    async fn wallet_balance_refresh_replaces_complete_snapshot_with_exact_precision() {
14287        let state = execution_rpc_state()
14288            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14289            .with_response_sequence(
14290                "eth_call",
14291                &[
14292                    CALL_BALANCE_WETH,
14293                    CALL_BALANCE_USDC,
14294                    CALL_BALANCE_WETH_UPDATED,
14295                    CALL_BALANCE_USDC_UPDATED,
14296                ],
14297            );
14298        let (mut client, _, _) =
14299            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_REPLACE").await;
14300        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14301        replace_exec_event_sender(sender);
14302        client.start().unwrap();
14303
14304        client.refresh_wallet_balances().await.unwrap();
14305        let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14306
14307        assert_eq!(balances.len(), 3);
14308        assert_eq!(balances[0].currency.code.as_str(), "ETH");
14309        assert_eq!(balances[0].currency.name.as_str(), "Ethereum");
14310        assert_eq!(balances[0].currency.precision, 18);
14311        assert_eq!(balances[0].total.raw, 1_000_000_000_000_000_000);
14312        assert_eq!(balances[0].free, balances[0].total);
14313        assert_eq!(balances[0].locked, Money::zero(balances[0].currency));
14314        assert_eq!(balances[1].currency.code.as_str(), "WETH");
14315        assert_eq!(balances[1].currency.name.as_str(), "Wrapped Ether");
14316        assert_eq!(balances[1].currency.precision, 18);
14317        assert_eq!(balances[1].total.raw, 1_234_567_890_123_456_789);
14318        assert_eq!(balances[1].free, balances[1].total);
14319        assert_eq!(balances[1].locked, Money::zero(balances[1].currency));
14320        assert_eq!(balances[2].currency.code.as_str(), "USDC");
14321        assert_eq!(balances[2].currency.name.as_str(), "USD Coin");
14322        assert_eq!(balances[2].currency.precision, 6);
14323        assert_eq!(balances[2].total.raw, 9_876_543_210_000_000_000);
14324        assert_eq!(balances[2].free, balances[2].total);
14325        assert_eq!(balances[2].locked, Money::zero(balances[2].currency));
14326
14327        client.refresh_wallet_balances().await.unwrap();
14328        let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14329
14330        assert_eq!(balances.len(), 3);
14331        assert_eq!(client.wallet_balance.lock().token_balances.len(), 2);
14332        assert_eq!(balances[0].total.raw, 0);
14333        assert_eq!(balances[1].total.raw, 2_000_000_000_000_000_000);
14334        assert_eq!(balances[2].total.raw, 12_345_670_000_000_000);
14335    }
14336
14337    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14338    #[tokio::test]
14339    async fn failed_connect_refresh_retains_snapshot_and_publishes_nothing() {
14340        let state = execution_rpc_state()
14341            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14342            .with_response_sequence(
14343                "eth_call",
14344                &[
14345                    CALL_BALANCE_WETH,
14346                    CALL_BALANCE_USDC,
14347                    CALL_BALANCE_WETH_UPDATED,
14348                    RPC_METHOD_NOT_FOUND,
14349                ],
14350            );
14351        let (mut client, _, _) =
14352            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_ATOMIC").await;
14353        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14354        replace_exec_event_sender(sender);
14355        client.start().unwrap();
14356        client.refresh_wallet_balances().await.unwrap();
14357        let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14358        receiver.try_recv().unwrap();
14359        // SAFETY: this variable name is unique to this test across the test binary
14360        unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_ATOMIC", TEST_PRIVATE_KEY) };
14361
14362        let error = client.connect().await.unwrap_err();
14363        let failed_address = Address::from_str(USDC).unwrap();
14364
14365        assert!(
14366            error.to_string().contains(&format!(
14367                "failed to fetch token balance for {failed_address}"
14368            )),
14369            "was: {error}"
14370        );
14371        assert!(!client.is_connected());
14372        assert!(client.signer.is_none());
14373        assert_eq!(
14374            client.wallet_balance.lock().as_account_balances().unwrap(),
14375            retained
14376        );
14377        assert!(matches!(
14378            receiver.try_recv(),
14379            Err(tokio::sync::mpsc::error::TryRecvError::Empty)
14380        ));
14381    }
14382
14383    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14384    #[tokio::test]
14385    async fn failed_connect_publication_retains_snapshot() {
14386        let state = execution_rpc_state()
14387            .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14388            .with_response_sequence(
14389                "eth_call",
14390                &[
14391                    CALL_BALANCE_WETH,
14392                    CALL_BALANCE_USDC,
14393                    CALL_BALANCE_WETH_UPDATED,
14394                    CALL_BALANCE_USDC_UPDATED,
14395                ],
14396            );
14397        let (mut client, _, _) =
14398            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_PUBLICATION").await;
14399        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14400        replace_exec_event_sender(sender);
14401        client.start().unwrap();
14402        client.refresh_wallet_balances().await.unwrap();
14403        let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14404        receiver.try_recv().unwrap();
14405        drop(receiver);
14406        // SAFETY: this variable name is unique to this test across the test binary
14407        unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_PUBLICATION", TEST_PRIVATE_KEY) };
14408
14409        let error = client.connect().await.unwrap_err();
14410
14411        assert!(
14412            error.to_string().contains("Failed to send account state"),
14413            "was: {error}"
14414        );
14415        assert!(!client.is_connected());
14416        assert!(client.signer.is_none());
14417        assert_eq!(
14418            client.wallet_balance.lock().as_account_balances().unwrap(),
14419            retained
14420        );
14421    }
14422
14423    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14424    #[tokio::test]
14425    async fn connect_and_repeated_query_publish_wallet_account_state() {
14426        let state = execution_rpc_state()
14427            .with_response_sequence("eth_call", &[CALL_BALANCE_WETH, CALL_BALANCE_USDC]);
14428        let (mut client, state, cache) =
14429            client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_ACCOUNT_STATE").await;
14430        let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14431        replace_exec_event_sender(sender);
14432        client.start().unwrap();
14433        // SAFETY: this variable name is unique to this test across the test binary
14434        unsafe { std::env::set_var("BLOCKCHAIN_TEST_ACCOUNT_STATE", TEST_PRIVATE_KEY) };
14435
14436        client.connect().await.unwrap();
14437
14438        let ExecutionEvent::Account(connected) = receiver.try_recv().unwrap() else {
14439            panic!("expected account state event")
14440        };
14441        assert_eq!(connected.account_id, AccountId::from("BLOCKCHAIN-001"));
14442        assert_eq!(connected.account_type, AccountType::Wallet);
14443        assert_eq!(connected.base_currency, None);
14444        assert_eq!(connected.balances.len(), 3);
14445        assert!(connected.margins.is_empty());
14446        assert!(connected.is_reported);
14447        cache.borrow_mut().update_account_state(&connected).unwrap();
14448
14449        let account = client.get_account().unwrap();
14450        assert!(matches!(account, AccountAny::Wallet(_)));
14451        assert_eq!(account.id(), AccountId::from("BLOCKCHAIN-001"));
14452        assert_eq!(account.last_event(), Some(connected.clone()));
14453
14454        let requests_before = state.recorded_requests().len();
14455        let query = || {
14456            QueryAccount::new(
14457                TraderId::from("TRADER-001"),
14458                Some(ClientId::from("BLOCKCHAIN-001")),
14459                AccountId::from("BLOCKCHAIN-001"),
14460                UUID4::new(),
14461                UnixNanos::default(),
14462                None,
14463                None,
14464            )
14465        };
14466        client.query_account(query()).unwrap();
14467        client.query_account(query()).unwrap();
14468
14469        let ExecutionEvent::Account(first_query) = receiver.try_recv().unwrap() else {
14470            panic!("expected first query account state event")
14471        };
14472        let ExecutionEvent::Account(second_query) = receiver.try_recv().unwrap() else {
14473            panic!("expected second query account state event")
14474        };
14475        assert!(connected.has_same_balances_and_margins(&first_query));
14476        assert!(first_query.has_same_balances_and_margins(&second_query));
14477        assert_eq!(first_query.account_id, connected.account_id);
14478        assert_eq!(first_query.account_type, connected.account_type);
14479        assert_eq!(first_query.base_currency, connected.base_currency);
14480        assert_eq!(first_query.is_reported, connected.is_reported);
14481        assert_ne!(first_query.event_id, second_query.event_id);
14482        assert_eq!(state.recorded_requests().len(), requests_before);
14483
14484        client.stop().unwrap();
14485        assert!(client.core.is_stopped());
14486        assert!(!client.is_connected());
14487        assert!(client.signer.is_none());
14488    }
14489
14490    #[tokio::test]
14491    async fn connect_rejects_on_chain_mismatch() {
14492        let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
14493        let (mut client, _) = client_with_mock_rpc(state).await;
14494
14495        let error = client.connect().await.unwrap_err();
14496
14497        assert!(
14498            error
14499                .to_string()
14500                .contains("Blockchain chain ID verification disagreed"),
14501            "was: {error}"
14502        );
14503    }
14504
14505    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14506    #[tokio::test]
14507    async fn connect_rejects_on_signer_wallet_mismatch() {
14508        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14509        let config = test_config_with_signer_env(
14510            format!("http://{addr}"),
14511            "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14512        );
14513        let mut client = test_client_from_config(config, test_pool());
14514        // Anvil development key #1 derives a different address than the configured wallet
14515        // SAFETY: this variable name is unique to this test across the test binary
14516        unsafe {
14517            std::env::set_var(
14518                "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14519                "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
14520            )
14521        };
14522
14523        let error = client.connect().await.unwrap_err();
14524
14525        assert!(
14526            error
14527                .to_string()
14528                .contains("does not match configured wallet address"),
14529            "was: {error}"
14530        );
14531    }
14532
14533    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14534    #[tokio::test]
14535    async fn connect_rejects_missing_trace_capability_before_signer_load() {
14536        let state = ready_rpc_state().with_parameter_response(
14537            "debug_traceTransaction",
14538            &B256::ZERO.to_string(),
14539            RPC_METHOD_NOT_FOUND,
14540        );
14541        let addr = start_mock_rpc_server(state.clone()).await;
14542        let config = test_config_with_signer_env(
14543            format!("http://{addr}"),
14544            "BLOCKCHAIN_TEST_TRACE_CAPABILITY",
14545        );
14546        let mut client = test_client_from_config(config, test_pool());
14547        // SAFETY: this variable name is unique to this test across the test binary
14548        unsafe { std::env::set_var("BLOCKCHAIN_TEST_TRACE_CAPABILITY", TEST_PRIVATE_KEY) };
14549
14550        let error = client.connect().await.unwrap_err();
14551
14552        assert!(
14553            error
14554                .to_string()
14555                .contains("Blockchain call trace capability verification is locally invalid"),
14556            "was: {error}"
14557        );
14558        assert!(client.signer.is_none());
14559        assert!(
14560            state
14561                .recorded_requests()
14562                .iter()
14563                .all(|request| request["method"] != "eth_getBalance")
14564        );
14565    }
14566
14567    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14568    #[tokio::test]
14569    async fn connect_initializes_signer_from_env() {
14570        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14571        let config =
14572            test_config_with_signer_env(format!("http://{addr}"), "BLOCKCHAIN_TEST_PRIVATE_KEY_OK");
14573        let mut client = test_client_from_config(config, test_pool());
14574        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14575        replace_exec_event_sender(sender);
14576        client.start().unwrap();
14577        // SAFETY: this variable name is unique to this test across the test binary
14578        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY_OK", TEST_PRIVATE_KEY) };
14579
14580        client.connect().await.unwrap();
14581
14582        let signer = client.signer.as_ref().unwrap();
14583        assert_eq!(
14584            signer.address(),
14585            address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
14586        );
14587    }
14588
14589    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14590    #[tokio::test]
14591    async fn reconnect_resumes_at_the_durable_finalized_tip() {
14592        let Some((admin_pool, pg_config)) =
14593            connect_test_postgres("verification ledger resume").await
14594        else {
14595            return;
14596        };
14597        let schema = format!("verification_ledger_resume_{}", std::process::id());
14598        setup_execution_schema(&admin_pool, &schema).await;
14599        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
14600        let options = options.options([("search_path", schema.clone())]);
14601        let database = connect_test_database(options).await.unwrap();
14602        database
14603            .ensure_execution_transaction_schema()
14604            .await
14605            .unwrap();
14606
14607        let state = ready_rpc_state();
14608        let addr = start_mock_rpc_server(state.clone()).await;
14609        let config = test_config_with_signer_env(
14610            format!("http://{addr}"),
14611            "BLOCKCHAIN_TEST_VERIFICATION_RESUME",
14612        );
14613        let mut client = test_client_from_config(config, test_pool());
14614        client.cache.database = Some(database);
14615        protect_test_storage(&mut client, &schema).await;
14616        let finalized_headers = [
14617            ExecutionVerifiedHeader {
14618                number: FIXTURE_BLOCK,
14619                hash: FIXTURE_BLOCK_HASH.to_string(),
14620                parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
14621                    .to_string(),
14622                timestamp: FIXTURE_BLOCK_TIMESTAMP,
14623                base_fee_per_gas: Some(100_000_000),
14624            },
14625            ExecutionVerifiedHeader {
14626                number: FIXTURE_BLOCK + 1,
14627                hash: B256::from([0x22; 32]).to_string(),
14628                parent_hash: FIXTURE_BLOCK_HASH.to_string(),
14629                timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
14630                base_fee_per_gas: Some(100_000_000),
14631            },
14632            ExecutionVerifiedHeader {
14633                number: FIXTURE_BLOCK + 2,
14634                hash: B256::from([0x33; 32]).to_string(),
14635                parent_hash: B256::from([0x22; 32]).to_string(),
14636                timestamp: FIXTURE_BLOCK_TIMESTAMP + 2,
14637                base_fee_per_gas: Some(100_000_000),
14638            },
14639        ];
14640        initialize_test_verification_ledger_with_headers(&client, &finalized_headers).await;
14641        let verification = client.config.verification.as_ref().unwrap();
14642        let position = client
14643            .cache
14644            .database
14645            .as_ref()
14646            .unwrap()
14647            .load_execution_verification_position(
14648                42_161,
14649                WALLET,
14650                &verification.manifest_version,
14651                &verification.manifest_digest,
14652            )
14653            .await
14654            .unwrap()
14655            .unwrap();
14656        let resume = client
14657            .cache
14658            .database
14659            .as_ref()
14660            .unwrap()
14661            .load_execution_verification_resume(
14662                42_161,
14663                WALLET,
14664                &verification.manifest_version,
14665                &verification.manifest_digest,
14666            )
14667            .await
14668            .unwrap()
14669            .unwrap();
14670        assert_eq!(position.next_canonical_nonce, 7);
14671        assert_eq!(position.revision, 0);
14672        assert_eq!(position.finalized_tip, finalized_headers[2]);
14673        assert_eq!(resume.next_canonical_nonce, 7);
14674        assert_eq!(resume.revision, 0);
14675        assert_eq!(resume.finalized_headers, finalized_headers);
14676
14677        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14678        replace_exec_event_sender(sender);
14679        client.start().unwrap();
14680        // SAFETY: this variable name is unique to this test across the test binary
14681        unsafe { std::env::set_var("BLOCKCHAIN_TEST_VERIFICATION_RESUME", TEST_PRIVATE_KEY) };
14682
14683        client.connect().await.unwrap();
14684
14685        let skipped_height = format!("0x{:x}", FIXTURE_BLOCK + 1);
14686        assert!(client.is_connected());
14687        assert!(state.recorded_requests().iter().all(|request| {
14688            request["method"] != "eth_getBlockByNumber"
14689                || request["params"][0].as_str() != Some(&skipped_height)
14690        }));
14691        let header_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14692            "SELECT COUNT(*) FROM {schema}.execution_verified_finalized_header"
14693        )))
14694        .fetch_one(&admin_pool)
14695        .await
14696        .unwrap();
14697        assert_eq!(header_count, 3);
14698
14699        drop(client);
14700        drop_execution_schema(&admin_pool, &schema).await;
14701    }
14702
14703    #[tokio::test]
14704    async fn disconnect_revokes_signer_and_blocks_execution() {
14705        let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
14706        client.core.set_connected();
14707        client.signer = Some(Arc::new(
14708            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14709        ));
14710
14711        client.disconnect().await.unwrap();
14712        let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
14713
14714        assert!(!client.is_connected());
14715        assert!(client.signer.is_none());
14716        assert!(
14717            error.to_string().contains("is not connected"),
14718            "was: {error}"
14719        );
14720        assert!(state.recorded_requests().is_empty());
14721    }
14722
14723    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14724    #[tokio::test]
14725    async fn connect_releases_stale_preparing_claim_after_tasks_finish() {
14726        let addr = start_mock_rpc_server(ready_rpc_state()).await;
14727        let config = test_config_with_signer_env(
14728            format!("http://{addr}"),
14729            "BLOCKCHAIN_TEST_RECONNECT_CLAIM",
14730        );
14731        let mut client = test_client_from_config(config, test_pool());
14732        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14733        replace_exec_event_sender(sender);
14734        client.start().unwrap();
14735        // SAFETY: this variable name is unique to this test across the test binary
14736        unsafe { std::env::set_var("BLOCKCHAIN_TEST_RECONNECT_CLAIM", TEST_PRIVATE_KEY) };
14737        *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Swap));
14738        client
14739            .pending_tasks
14740            .spawn(async {})
14741            .expect("stale task spawn");
14742
14743        client.connect().await.unwrap();
14744
14745        assert!(client.in_flight.lock().is_none());
14746    }
14747
14748    #[allow(unsafe_code)] // env-var mutation in tests; unique var names avoid cross-test races
14749    #[tokio::test]
14750    async fn reconnect_after_aborted_submission_releases_preparing_claim() {
14751        // The swap task blocks in its first RPC; disconnect aborts it mid-preparation, and the
14752        // following connect awaits termination and releases the stale claim
14753        let state = ready_rpc_state().with_sleep("eth_getBlockByNumber", Duration::from_secs(30));
14754        let Some((admin_pool, schema, mut client, state, _)) =
14755            swap_client_with_database("execution_reconnect_claim_test", state).await
14756        else {
14757            return;
14758        };
14759        let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14760        replace_exec_event_sender(sender);
14761        client.start().unwrap();
14762        // SAFETY: this variable name is unique to this test across the test binary
14763        unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
14764
14765        let order = test_market_sell_order(test_pool().instrument_id);
14766        client.submit_order(submit_order_cmd(&order)).unwrap();
14767        await_recorded_requests(&state, "eth_getBlockByNumber", 1).await;
14768        assert!(client.in_flight.lock().is_some());
14769
14770        client.disconnect().await.unwrap();
14771        let ready_addr = start_mock_rpc_server(ready_rpc_state()).await;
14772        let ready = test_client(format!("http://{ready_addr}"));
14773        client.http_rpc_client = ready.http_rpc_client;
14774        client.verification = ready.verification;
14775        client.connect().await.unwrap();
14776
14777        assert!(client.in_flight.lock().is_none());
14778
14779        drop_execution_schema(&admin_pool, &schema).await;
14780    }
14781
14782    #[tokio::test]
14783    async fn poll_for_receipt_returns_none_after_exhaustion() {
14784        let state = ready_rpc_state().with_response("eth_getTransactionReceipt", RECEIPT_NULL);
14785        let (client, state) = client_with_mock_rpc(state).await;
14786
14787        let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14788            .await
14789            .unwrap();
14790
14791        assert!(receipt.is_none());
14792        let requests = state.recorded_requests();
14793        assert_eq!(requests.len(), 3);
14794    }
14795
14796    #[tokio::test]
14797    async fn poll_for_receipt_returns_last_error_when_every_poll_fails() {
14798        let state =
14799            ready_rpc_state().with_response("eth_getTransactionReceipt", RPC_METHOD_NOT_FOUND);
14800        let (client, state) = client_with_mock_rpc(state).await;
14801
14802        let error = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14803            .await
14804            .unwrap_err();
14805
14806        assert_eq!(
14807            error.to_string(),
14808            "eth_getTransactionReceipt RPC error -32601"
14809        );
14810        let requests = state.recorded_requests();
14811        assert_eq!(requests.len(), 3);
14812    }
14813
14814    #[tokio::test]
14815    async fn poll_for_receipt_returns_none_after_pending_then_errors() {
14816        let state = ready_rpc_state().with_response_sequence(
14817            "eth_getTransactionReceipt",
14818            &[RECEIPT_NULL, RPC_METHOD_NOT_FOUND, RPC_METHOD_NOT_FOUND],
14819        );
14820        let (client, state) = client_with_mock_rpc(state).await;
14821
14822        let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14823            .await
14824            .unwrap();
14825
14826        assert!(receipt.is_none());
14827        let requests = state.recorded_requests();
14828        assert_eq!(requests.len(), 3);
14829    }
14830
14831    #[tokio::test]
14832    async fn cancellation_during_persistence_keeps_in_flight_slot() {
14833        let Some((admin_pool, pg_config)) = connect_test_postgres("persistence cancellation").await
14834        else {
14835            return;
14836        };
14837
14838        let schema = format!("execution_persist_cancel_test_{}", std::process::id());
14839        setup_execution_schema(&admin_pool, &schema).await;
14840
14841        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
14842        let db_options = db_options.options([("search_path", schema.clone())]);
14843        let database = connect_test_database(db_options).await.unwrap();
14844
14845        let state = signing_rpc_state();
14846        let addr = start_mock_rpc_server(state.clone()).await;
14847
14848        let mut client = test_client(format!("http://{addr}"));
14849        client.cache.database = Some(database);
14850        // Mirror the connect-time migration: tests create the pre-submission table shape
14851        client
14852            .cache
14853            .ensure_execution_transaction_schema()
14854            .await
14855            .unwrap();
14856        protect_test_storage(&mut client, &schema).await;
14857        initialize_test_verification_ledger(&client).await;
14858        client.signer = Some(Arc::new(
14859            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14860        ));
14861        client.core.set_connected();
14862
14863        let advisory_lock = i64::from(std::process::id());
14864
14865        for statement in [
14866            format!(
14867                "CREATE FUNCTION {schema}.block_execution_hash_insert() RETURNS trigger \
14868                 LANGUAGE plpgsql AS 'BEGIN PERFORM pg_advisory_xact_lock({advisory_lock}); \
14869                 RETURN NEW; END'"
14870            ),
14871            format!(
14872                "CREATE TRIGGER block_execution_hash_insert BEFORE INSERT ON \
14873                 {schema}.execution_transaction_hash FOR EACH ROW EXECUTE FUNCTION \
14874                 {schema}.block_execution_hash_insert()"
14875            ),
14876        ] {
14877            sqlx::query(sqlx::AssertSqlSafe(statement))
14878                .execute(&admin_pool)
14879                .await
14880                .unwrap();
14881        }
14882        let mut lock_transaction = admin_pool.begin().await.unwrap();
14883        sqlx::query("SELECT pg_advisory_xact_lock($1)")
14884            .bind(advisory_lock)
14885            .execute(&mut *lock_transaction)
14886            .await
14887            .unwrap();
14888
14889        let value = U256::from(1_000_000_000_000_000u64);
14890        let in_flight = Arc::clone(&client.in_flight);
14891        let mut wrap = Box::pin(client.wrap(value));
14892        tokio::time::timeout(Duration::from_secs(2), async {
14893            loop {
14894                tokio::select! {
14895                    result = &mut wrap => {
14896                        panic!("persistence completed while the table was locked: {result:?}")
14897                    }
14898                    () = tokio::time::sleep(Duration::from_millis(1)) => {}
14899                }
14900
14901                if matches!(*in_flight.lock(), Some(InFlightSlot::AwaitingFinality(_))) {
14902                    break;
14903                }
14904            }
14905        })
14906        .await
14907        .unwrap();
14908        drop(wrap);
14909        lock_transaction.rollback().await.unwrap();
14910
14911        let slot = *client.in_flight.lock();
14912        let second_error = client
14913            .wrap(U256::from(2_000_000_000_000_000u64))
14914            .await
14915            .unwrap_err();
14916        let broadcasts = state
14917            .recorded_requests()
14918            .into_iter()
14919            .filter(|request| request["method"] == "eth_sendRawTransaction")
14920            .count();
14921        let status: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14922            "SELECT status FROM {schema}.execution_intent"
14923        )))
14924        .fetch_one(&admin_pool)
14925        .await
14926        .unwrap();
14927
14928        assert!(matches!(slot, Some(InFlightSlot::AwaitingFinality(_))));
14929        assert_eq!(status, "prepared");
14930        assert!(
14931            second_error.to_string().contains("awaiting finality"),
14932            "was: {second_error}"
14933        );
14934        assert_eq!(broadcasts, 0);
14935
14936        drop_execution_schema(&admin_pool, &schema).await;
14937    }
14938
14939    #[tokio::test]
14940    async fn reservation_failure_releases_preparing_slot() {
14941        let state = signing_rpc_state();
14942        let Some((admin_pool, schema, mut client, state)) =
14943            execution_client_with_database("execution_reservation_fail_test", state).await
14944        else {
14945            return;
14946        };
14947        sqlx::query(sqlx::AssertSqlSafe(format!(
14948            "DROP TABLE {schema}.execution_intent CASCADE"
14949        )))
14950        .execute(&admin_pool)
14951        .await
14952        .unwrap();
14953
14954        let error = client
14955            .wrap(U256::from(1_000_000_000_000_000u64))
14956            .await
14957            .unwrap_err();
14958        let retry_error = client
14959            .wrap(U256::from(2_000_000_000_000_000u64))
14960            .await
14961            .unwrap_err();
14962        let broadcasts = state
14963            .recorded_requests()
14964            .into_iter()
14965            .filter(|request| request["method"] == "eth_sendRawTransaction")
14966            .count();
14967
14968        assert_eq!(
14969            error.to_string(),
14970            "Execution intent reservation failed before commit"
14971        );
14972        assert!(reservation_failure_proven_not_committed(&error));
14973        assert_eq!(
14974            retry_error.to_string(),
14975            "Execution intent reservation failed before commit"
14976        );
14977        assert_eq!(broadcasts, 0);
14978        assert!(client.in_flight.lock().is_none());
14979
14980        drop_execution_schema(&admin_pool, &schema).await;
14981    }
14982
14983    #[tokio::test]
14984    async fn reservation_commit_failure_keeps_preparing_slot() {
14985        let state = signing_rpc_state();
14986        let Some((admin_pool, schema, mut client, state)) =
14987            execution_client_with_database("execution_reservation_commit_fail_test", state).await
14988        else {
14989            return;
14990        };
14991        install_reservation_commit_rejection(&admin_pool, &schema).await;
14992
14993        let error = client
14994            .wrap(U256::from(1_000_000_000_000_000u64))
14995            .await
14996            .unwrap_err();
14997        let slot = *client.in_flight.lock();
14998        let second_error = client
14999            .wrap(U256::from(2_000_000_000_000_000u64))
15000            .await
15001            .unwrap_err();
15002        let requests = state.recorded_requests();
15003        let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15004            "SELECT COUNT(*) FROM {schema}.execution_intent"
15005        )))
15006        .fetch_one(&admin_pool)
15007        .await
15008        .unwrap();
15009        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15010            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
15011        )))
15012        .fetch_one(&admin_pool)
15013        .await
15014        .unwrap();
15015
15016        assert_eq!(
15017            error.to_string(),
15018            "Execution intent reservation commit outcome is unknown; reconciliation is required"
15019        );
15020        assert!(!reservation_failure_proven_not_committed(&error));
15021        assert!(matches!(
15022            slot,
15023            Some(InFlightSlot::Preparing(TransactionPurpose::Wrap))
15024        ));
15025        assert!(
15026            second_error.to_string().contains("being prepared"),
15027            "was: {second_error}"
15028        );
15029        assert_eq!(intent_count, 0);
15030        assert_eq!(signed_count, 0);
15031        assert!(
15032            requests
15033                .iter()
15034                .all(|request| request["method"] != "eth_getTransactionCount")
15035        );
15036        assert!(
15037            requests
15038                .iter()
15039                .all(|request| request["method"] != "eth_sendRawTransaction")
15040        );
15041
15042        drop_execution_schema(&admin_pool, &schema).await;
15043    }
15044
15045    #[tokio::test]
15046    async fn persistence_failure_keeps_unbroadcast_slot() {
15047        let state = signing_rpc_state();
15048        let Some((admin_pool, schema, mut client, state)) =
15049            execution_client_with_database("execution_persist_fail_test", state).await
15050        else {
15051            return;
15052        };
15053        sqlx::query(sqlx::AssertSqlSafe(format!(
15054            "ALTER TABLE {schema}.execution_transaction_hash \
15055             ADD CONSTRAINT execution_hash_reject CHECK (FALSE)"
15056        )))
15057        .execute(&admin_pool)
15058        .await
15059        .unwrap();
15060
15061        let value = U256::from(1_000_000_000_000_000u64);
15062        let error = client.wrap(value).await.unwrap_err();
15063        let in_flight = awaiting_in_flight(&client);
15064        let second_error = client
15065            .wrap(U256::from(2_000_000_000_000_000u64))
15066            .await
15067            .unwrap_err();
15068        let broadcasts = state
15069            .recorded_requests()
15070            .into_iter()
15071            .filter(|request| request["method"] == "eth_sendRawTransaction")
15072            .count();
15073        let expected_hash = expected_wrap_tx_hash(value).await;
15074
15075        let error_message = error.to_string();
15076        assert!(error_message.starts_with(&format!(
15077            "Failed to persist transaction {expected_hash}: Failed to persist signed transaction"
15078        )), "was: {error_message}");
15079        assert!(
15080            error_message.ends_with("the in-flight slot stays occupied"),
15081            "was: {error_message}"
15082        );
15083        assert_eq!(in_flight.nonce, 7);
15084        assert_eq!(in_flight.tx_hash, expected_hash);
15085        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15086        assert!(
15087            second_error.to_string().contains("still awaiting finality"),
15088            "was: {second_error}"
15089        );
15090        assert_eq!(broadcasts, 0);
15091
15092        drop_execution_schema(&admin_pool, &schema).await;
15093    }
15094
15095    #[tokio::test]
15096    async fn cancellation_after_dispatch_keeps_record_and_in_flight_slot() {
15097        let state = signing_rpc_state().with_sleep(
15098            "eth_sendRawTransaction",
15099            Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15100        );
15101        let Some((admin_pool, schema, mut client, state)) =
15102            execution_client_with_database("execution_cancel_test", state).await
15103        else {
15104            return;
15105        };
15106
15107        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15108        tokio::select! {
15109            result = &mut wrap => panic!("broadcast completed before cancellation: {result:?}"),
15110            () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15111        }
15112        drop(wrap);
15113
15114        let in_flight = awaiting_in_flight(&client);
15115        let error = client
15116            .wrap(U256::from(2_000_000_000_000_000u64))
15117            .await
15118            .unwrap_err();
15119        let record = client
15120            .cache
15121            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15122            .await
15123            .unwrap()
15124            .unwrap();
15125        let broadcasts = state
15126            .recorded_requests()
15127            .into_iter()
15128            .filter(|request| request["method"] == "eth_sendRawTransaction")
15129            .count();
15130
15131        assert!(
15132            error.to_string().contains("still awaiting finality"),
15133            "was: {error}"
15134        );
15135        assert_eq!(record.nonce, 7);
15136        assert_eq!(record.purpose, "wrap");
15137        assert_eq!(record.status, "broadcast");
15138        assert_eq!(broadcasts, 1);
15139
15140        drop_execution_schema(&admin_pool, &schema).await;
15141    }
15142
15143    #[tokio::test]
15144    async fn cancellation_during_receipt_polling_keeps_record_and_in_flight_slot() {
15145        let state = signing_rpc_state()
15146            .with_send_raw_transaction_echo()
15147            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
15148            .with_sleep(
15149                "eth_getTransactionReceipt",
15150                Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15151            );
15152        let Some((admin_pool, schema, mut client, state)) =
15153            execution_client_with_database("execution_receipt_cancel_test", state).await
15154        else {
15155            return;
15156        };
15157
15158        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15159        tokio::select! {
15160            result = &mut wrap => panic!("receipt polling completed before cancellation: {result:?}"),
15161            () = await_recorded_requests(&state, "eth_getTransactionReceipt", 3) => {}
15162        }
15163        drop(wrap);
15164
15165        let in_flight = awaiting_in_flight(&client);
15166        let second_error = client
15167            .wrap(U256::from(2_000_000_000_000_000u64))
15168            .await
15169            .unwrap_err();
15170        let record = client
15171            .cache
15172            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15173            .await
15174            .unwrap()
15175            .unwrap();
15176        let requests = state.recorded_requests();
15177
15178        assert_eq!(in_flight.nonce, 7);
15179        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15180        assert!(
15181            second_error.to_string().contains("still awaiting finality"),
15182            "was: {second_error}"
15183        );
15184        assert_eq!(record.nonce, 7);
15185        assert_eq!(record.transaction_hash, in_flight.tx_hash.to_string());
15186        assert_eq!(record.purpose, "wrap");
15187        assert_eq!(record.status, "broadcast");
15188        assert_eq!(
15189            requests
15190                .iter()
15191                .filter(|request| request["method"] == "eth_sendRawTransaction")
15192                .count(),
15193            1
15194        );
15195        assert_eq!(
15196            requests
15197                .iter()
15198                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15199                .count(),
15200            3
15201        );
15202
15203        drop_execution_schema(&admin_pool, &schema).await;
15204    }
15205
15206    #[tokio::test]
15207    async fn rejected_broadcast_stays_dropped_and_occupied() {
15208        let state = signing_rpc_state()
15209            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15210            .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
15211        let Some((admin_pool, schema, mut client, state)) =
15212            execution_client_with_database("execution_rejected_test", state).await
15213        else {
15214            return;
15215        };
15216
15217        let error = client
15218            .wrap(U256::from(1_000_000_000_000_000u64))
15219            .await
15220            .unwrap_err();
15221        let (purpose, status): (String, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15222            "SELECT purpose, status FROM {schema}.execution_intent"
15223        )))
15224        .fetch_one(&admin_pool)
15225        .await
15226        .unwrap();
15227        let broadcasts = state
15228            .recorded_requests()
15229            .into_iter()
15230            .filter(|request| request["method"] == "eth_sendRawTransaction")
15231            .count();
15232
15233        assert!(
15234            error.to_string().contains("Timed out awaiting finality"),
15235            "was: {error}"
15236        );
15237        assert!(client.in_flight.lock().is_some());
15238        assert_eq!(purpose, "wrap");
15239        assert_eq!(status, "dropped");
15240        assert_eq!(broadcasts, 1);
15241
15242        drop_execution_schema(&admin_pool, &schema).await;
15243    }
15244
15245    #[tokio::test]
15246    async fn legacy_table_loss_does_not_release_rejected_broadcast() {
15247        let state = signing_rpc_state()
15248            .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15249            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15250            .with_sleep("eth_sendRawTransaction", Duration::from_secs(1));
15251        let Some((admin_pool, schema, mut client, state)) =
15252            execution_client_with_database("execution_rejected_update_test", state).await
15253        else {
15254            return;
15255        };
15256
15257        let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15258        tokio::select! {
15259            result = &mut wrap => panic!("broadcast completed before database failure: {result:?}"),
15260            () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15261        }
15262        sqlx::query(sqlx::AssertSqlSafe(format!(
15263            "DROP TABLE {schema}.execution_transaction"
15264        )))
15265        .execute(&admin_pool)
15266        .await
15267        .unwrap();
15268
15269        let error = wrap.await.unwrap_err();
15270
15271        assert!(
15272            error.to_string().contains("Timed out awaiting finality"),
15273            "was: {error}"
15274        );
15275        let in_flight = awaiting_in_flight(&client);
15276        assert_eq!(in_flight.nonce, 7);
15277        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15278
15279        drop_execution_schema(&admin_pool, &schema).await;
15280    }
15281
15282    #[tokio::test]
15283    async fn finalized_receipt_keeps_slot_when_status_update_fails() {
15284        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
15285        let block = finalized_wrap_block(expected_hash);
15286        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15287        let state = with_finalized_identity(
15288            signing_rpc_state()
15289                .with_send_raw_transaction_echo()
15290                .with_response("eth_getTransactionReceipt", &receipt)
15291                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15292                .with_call_response_sequence(
15293                    BALANCE_OF_SELECTOR,
15294                    &[
15295                        CALL_BALANCE,
15296                        CALL_BALANCE,
15297                        CALL_BALANCE,
15298                        CALL_BALANCE,
15299                        CALL_BALANCE,
15300                        CALL_BALANCE,
15301                        CALL_BALANCE_AFTER_WRAP,
15302                        CALL_BALANCE_AFTER_WRAP,
15303                        CALL_BALANCE_AFTER_WRAP,
15304                    ],
15305                ),
15306            &block,
15307            &receipt,
15308        );
15309        let Some((admin_pool, schema, mut client, state)) =
15310            execution_client_with_database("execution_finalized_update_test", state).await
15311        else {
15312            return;
15313        };
15314
15315        for statement in [
15316            format!(
15317                "CREATE FUNCTION {schema}.reject_finalized_status() RETURNS trigger \
15318                 LANGUAGE plpgsql AS 'BEGIN IF NEW.status = ''finalized'' THEN \
15319                 RAISE EXCEPTION ''test finalized status rejection''; END IF; \
15320                 RETURN NEW; END'"
15321            ),
15322            format!(
15323                "CREATE TRIGGER reject_finalized_status BEFORE UPDATE ON \
15324                 {schema}.execution_transaction_hash FOR EACH ROW \
15325                 EXECUTE FUNCTION {schema}.reject_finalized_status()"
15326            ),
15327        ] {
15328            sqlx::query(sqlx::AssertSqlSafe(statement))
15329                .execute(&admin_pool)
15330                .await
15331                .unwrap();
15332        }
15333
15334        let error = client
15335            .wrap(U256::from(1_000_000_000_000_000u64))
15336            .await
15337            .unwrap_err();
15338        let in_flight = awaiting_in_flight(&client);
15339        let requests = state.recorded_requests();
15340
15341        assert!(
15342            error
15343                .to_string()
15344                .contains("test finalized status rejection"),
15345            "was: {error}"
15346        );
15347        assert_eq!(in_flight.nonce, 7);
15348        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15349        assert_eq!(
15350            requests
15351                .iter()
15352                .filter(|request| request["method"] == "eth_sendRawTransaction")
15353                .count(),
15354            1
15355        );
15356        assert_eq!(
15357            requests
15358                .iter()
15359                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15360                .count(),
15361            3
15362        );
15363
15364        drop_execution_schema(&admin_pool, &schema).await;
15365    }
15366
15367    #[tokio::test]
15368    async fn broadcast_timeout_marks_dropped_and_keeps_ownership() {
15369        let state = signing_rpc_state()
15370            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15371            .with_sleep(
15372                "eth_sendRawTransaction",
15373                Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15374            );
15375        let Some((admin_pool, schema, mut client, _)) =
15376            execution_client_with_database("execution_timeout_test", state).await
15377        else {
15378            return;
15379        };
15380
15381        let error = client
15382            .wrap(U256::from(1_000_000_000_000_000u64))
15383            .await
15384            .unwrap_err();
15385
15386        assert!(
15387            error.to_string().contains("Timed out awaiting finality"),
15388            "was: {error}"
15389        );
15390        let in_flight = awaiting_in_flight(&client);
15391        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15392        assert_eq!(in_flight.nonce, 7);
15393
15394        // The persisted record stays active for reconciliation instead of rebroadcasting.
15395        let record = client
15396            .cache
15397            .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15398            .await
15399            .unwrap()
15400            .unwrap();
15401        assert_eq!(record.status, "dropped");
15402
15403        drop_execution_schema(&admin_pool, &schema).await;
15404    }
15405
15406    #[tokio::test]
15407    async fn restart_reconciles_dropped_transaction_without_rebroadcast() {
15408        let initial_state = broadcast_rpc_state()
15409            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15410            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15411        let Some((admin_pool, schema, mut first_client, _)) =
15412            execution_client_with_database("execution_restart_test", initial_state).await
15413        else {
15414            return;
15415        };
15416        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
15417        let error = first_client
15418            .wrap(U256::from(1_000_000_000_000_000_u64))
15419            .await
15420            .unwrap_err();
15421        assert!(
15422            error.to_string().contains("Timed out awaiting finality"),
15423            "was: {error}"
15424        );
15425        let database = first_client.cache.database.as_ref().unwrap().clone();
15426        let payload_keys = first_client.payload_keys.clone();
15427        drop(first_client);
15428
15429        let block = finalized_wrap_block(expected_hash);
15430        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15431        let restart_state = with_finalized_identity(
15432            execution_rpc_state()
15433                .with_response("eth_getTransactionReceipt", &receipt)
15434                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15435                .with_response_sequence(
15436                    "eth_call",
15437                    &[
15438                        CALL_BALANCE,
15439                        CALL_BALANCE,
15440                        CALL_BALANCE,
15441                        CALL_BALANCE_AFTER_WRAP,
15442                        CALL_BALANCE_AFTER_WRAP,
15443                        CALL_BALANCE_AFTER_WRAP,
15444                    ],
15445                ),
15446            &block,
15447            &receipt,
15448        );
15449        let addr = start_mock_rpc_server(restart_state.clone()).await;
15450        let mut restarted = test_client(format!("http://{addr}"));
15451        restarted.cache.database = Some(database);
15452        restarted.payload_keys = payload_keys;
15453        restarted.signer = Some(Arc::new(
15454            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
15455        ));
15456
15457        restarted.reconcile_unresolved_execution().await.unwrap();
15458        restarted.reconcile_unresolved_execution().await.unwrap();
15459
15460        let record = restarted
15461            .cache
15462            .get_execution_transaction(42161, &expected_hash.to_string())
15463            .await
15464            .unwrap()
15465            .unwrap();
15466        let requests = restart_state.recorded_requests();
15467        assert_eq!(record.status, "finalized");
15468        assert!(restarted.in_flight.lock().is_none());
15469        assert_eq!(
15470            requests
15471                .iter()
15472                .filter(|request| request["method"] == "eth_getTransactionReceipt")
15473                .count(),
15474            6
15475        );
15476        assert_eq!(
15477            requests
15478                .iter()
15479                .filter(|request| {
15480                    request["method"] == "eth_call"
15481                        && request["params"][0]["data"]
15482                            .as_str()
15483                            .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
15484                })
15485                .count(),
15486            6
15487        );
15488        assert_eq!(
15489            requests
15490                .iter()
15491                .filter(|request| request["method"] == "eth_sendRawTransaction")
15492                .count(),
15493            0
15494        );
15495
15496        drop_execution_schema(&admin_pool, &schema).await;
15497    }
15498
15499    fn migration_test_intent(id: i64) -> ExecutionIntentRow {
15500        ExecutionIntentRow {
15501            id,
15502            schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
15503            chain_id: 42_161,
15504            wallet_address: WALLET.to_string(),
15505            nonce: None,
15506            purpose: "wrap".to_string(),
15507            status: "prepared".to_string(),
15508            client_order_id: None,
15509            trader_id: None,
15510            strategy_id: None,
15511            account_id: None,
15512            instrument_id: None,
15513            pool_address: None,
15514            transaction_to: WETH.to_string(),
15515            transaction_input: "0xd0e30db0".to_string(),
15516            transaction_value: "1".to_string(),
15517            amount_in: None,
15518            created_block: FIXTURE_BLOCK,
15519            acknowledgement_emitted: false,
15520            fill_emitted: false,
15521            terminal_emitted: false,
15522            active: true,
15523        }
15524    }
15525
15526    fn migration_nonce_verification() -> Verified<u64> {
15527        Verified {
15528            value: 7,
15529            read: crate::rpc::verification::VerificationRead::TransactionCount,
15530            provider_ids: [
15531                "authoritative".to_string(),
15532                "verifier-a".to_string(),
15533                "verifier-b".to_string(),
15534            ],
15535            normalized_value_digest: keccak256(7_u64.to_be_bytes()),
15536        }
15537    }
15538
15539    fn migration_finalized_header() -> VerifiedBlockHeader {
15540        VerifiedBlockHeader {
15541            number: FIXTURE_BLOCK,
15542            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15543            parent_hash: B256::from_str(
15544                "0x0000000000000000000000000000000000000000000000000000000000000001",
15545            )
15546            .unwrap(),
15547            timestamp: FIXTURE_BLOCK_TIMESTAMP,
15548            base_fee_per_gas: Some(100_000_000),
15549        }
15550    }
15551
15552    #[tokio::test]
15553    async fn verification_migration_recovers_prepared_unassigned_intent() {
15554        let client = test_client("http://127.0.0.1:1".to_string());
15555        let snapshot = ExecutionVerificationMigrationSnapshot {
15556            intents: vec![migration_test_intent(1)],
15557            hashes: Vec::new(),
15558        };
15559        let finalized = migration_finalized_header();
15560        let migration = client
15561            .build_execution_verification_migration(
15562                snapshot,
15563                finalized,
15564                &[finalized],
15565                &migration_nonce_verification(),
15566            )
15567            .await
15568            .unwrap();
15569
15570        assert_eq!(migration.records.len(), 1);
15571        let record = &migration.records[0];
15572        assert_eq!(record.intent_id, 1);
15573        assert_eq!(record.nonce, None);
15574        assert_eq!(record.transaction_hash, None);
15575        assert!(record.recover_prepared);
15576        assert_eq!(record.decisions.len(), 1);
15577    }
15578
15579    #[tokio::test]
15580    async fn verification_migration_rejects_inconsistent_released_history() {
15581        let client = test_client("http://127.0.0.1:1".to_string());
15582        let finalized = migration_finalized_header();
15583        let nonce_verification = migration_nonce_verification();
15584        let mut missing_marker = migration_test_intent(1);
15585        missing_marker.active = false;
15586        missing_marker.status = "finalized".to_string();
15587        missing_marker.nonce = Some(6);
15588        let error = client
15589            .build_execution_verification_migration(
15590                ExecutionVerificationMigrationSnapshot {
15591                    intents: vec![missing_marker],
15592                    hashes: Vec::new(),
15593                },
15594                finalized,
15595                &[finalized],
15596                &nonce_verification,
15597            )
15598            .await
15599            .err()
15600            .unwrap();
15601        assert!(
15602            error.to_string().contains("has no durable event marker"),
15603            "was: {error}"
15604        );
15605
15606        let mut first = migration_test_intent(1);
15607        first.active = false;
15608        first.status = "recoverable".to_string();
15609        first.nonce = Some(6);
15610        let mut second = first.clone();
15611        second.id = 2;
15612        let error = client
15613            .build_execution_verification_migration(
15614                ExecutionVerificationMigrationSnapshot {
15615                    intents: vec![first, second],
15616                    hashes: Vec::new(),
15617                },
15618                finalized,
15619                &[finalized],
15620                &nonce_verification,
15621            )
15622            .await
15623            .err()
15624            .unwrap();
15625        assert!(
15626            error
15627                .to_string()
15628                .contains("duplicate signer nonce ownership"),
15629            "was: {error}"
15630        );
15631    }
15632
15633    #[tokio::test]
15634    async fn verification_migration_reconstructs_consumed_active_intent() {
15635        let expected_hash = expected_wrap_tx_hash(U256::from(1u64)).await;
15636        let mut block_value: serde_json::Value =
15637            serde_json::from_str(&finalized_wrap_block(expected_hash)).unwrap();
15638        block_value["result"]["transactions"][0]["value"] = serde_json::json!("0x1");
15639        let block = block_value.to_string();
15640        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15641        let state = with_finalized_identity(
15642            execution_rpc_state()
15643                .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
15644                .with_response("eth_getTransactionReceipt", &receipt)
15645                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
15646            &block,
15647            &receipt,
15648        );
15649        let Some((admin_pool, pg_config)) =
15650            connect_test_postgres("verification_migration_reconstructs_consumed_active_intent")
15651                .await
15652        else {
15653            return;
15654        };
15655        let schema = format!(
15656            "verification_migration_reconstructs_consumed_active_intent_{}",
15657            std::process::id()
15658        );
15659        setup_execution_schema(&admin_pool, &schema).await;
15660        let options: sqlx::postgres::PgConnectOptions = pg_config.into();
15661        let database = connect_test_database(options.options([("search_path", schema.clone())]))
15662            .await
15663            .unwrap();
15664        let addr = start_mock_rpc_server(state).await;
15665        let (mut client, _) = swap_client_with_cache(test_config(format!("http://{addr}")));
15666        client.cache.database = Some(database.clone());
15667        client
15668            .cache
15669            .ensure_execution_transaction_schema()
15670            .await
15671            .unwrap();
15672        let (intent, persisted_hash, _) = persist_test_wrap_broadcast(&database, None).await;
15673        protect_test_storage(&mut client, &schema).await;
15674        assert_eq!(persisted_hash, expected_hash);
15675
15676        let snapshot = database
15677            .load_execution_verification_migration_snapshot(42_161, WALLET)
15678            .await
15679            .unwrap();
15680        let nonce_verification = required_verification(
15681            client
15682                .verification
15683                .verify_transaction_count(&Address::from_str(WALLET).unwrap(), FIXTURE_BLOCK + 1)
15684                .await,
15685            "test migration nonce",
15686        )
15687        .unwrap();
15688        let headers = [
15689            VerifiedBlockHeader {
15690                number: FIXTURE_BLOCK,
15691                hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15692                parent_hash: B256::from_str(
15693                    "0x0000000000000000000000000000000000000000000000000000000000000001",
15694                )
15695                .unwrap(),
15696                timestamp: FIXTURE_BLOCK_TIMESTAMP,
15697                base_fee_per_gas: Some(100_000_000),
15698            },
15699            VerifiedBlockHeader {
15700                number: FIXTURE_BLOCK + 1,
15701                hash: B256::from([0x22; 32]),
15702                parent_hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15703                timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
15704                base_fee_per_gas: Some(100_000_000),
15705            },
15706        ];
15707        let migration = client
15708            .build_execution_verification_migration(
15709                snapshot,
15710                headers[1],
15711                &headers,
15712                &nonce_verification,
15713            )
15714            .await
15715            .unwrap();
15716        let finalized_headers = headers
15717            .iter()
15718            .map(|header| ExecutionVerifiedHeader {
15719                number: header.number,
15720                hash: header.hash.to_string(),
15721                parent_hash: header.parent_hash.to_string(),
15722                timestamp: header.timestamp,
15723                base_fee_per_gas: header.base_fee_per_gas,
15724            })
15725            .collect::<Vec<_>>();
15726        let decisions = [verification_decision(
15727            &nonce_verification,
15728            Some(FIXTURE_BLOCK + 1),
15729            Some(FIXTURE_BLOCK + 1),
15730        )];
15731
15732        initialize_test_verification_migration(
15733            &client,
15734            &finalized_headers,
15735            8,
15736            &decisions,
15737            &migration,
15738        )
15739        .await;
15740
15741        let migrated = database.get_execution_intent(intent.id).await.unwrap();
15742        let resume = database
15743            .load_execution_verification_resume(
15744                42_161,
15745                WALLET,
15746                &client
15747                    .config
15748                    .verification
15749                    .as_ref()
15750                    .unwrap()
15751                    .manifest_version,
15752                &client.config.verification.as_ref().unwrap().manifest_digest,
15753            )
15754            .await
15755            .unwrap()
15756            .unwrap();
15757        let evidence: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15758            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
15759             WHERE intent_id = $1 AND decision_class = 'migration'"
15760        )))
15761        .bind(intent.id)
15762        .fetch_one(&admin_pool)
15763        .await
15764        .unwrap();
15765
15766        assert_eq!(migrated.status, "finalized");
15767        assert!(migrated.active);
15768        assert_eq!(resume.next_canonical_nonce, 8);
15769        assert!(evidence >= 5);
15770
15771        drop_execution_schema(&admin_pool, &schema).await;
15772    }
15773
15774    #[tokio::test]
15775    async fn verification_resume_accepts_one_consumed_owned_nonce() {
15776        let Some((admin_pool, schema, client, _)) = execution_client_with_database(
15777            "verification_resume_owned_nonce_test",
15778            ready_rpc_state(),
15779        )
15780        .await
15781        else {
15782            return;
15783        };
15784        let database = client.cache.database.as_ref().unwrap();
15785        persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15786        let resume = database
15787            .load_execution_verification_resume(
15788                42_161,
15789                WALLET,
15790                &client
15791                    .config
15792                    .verification
15793                    .as_ref()
15794                    .unwrap()
15795                    .manifest_version,
15796                &client.config.verification.as_ref().unwrap().manifest_digest,
15797            )
15798            .await
15799            .unwrap()
15800            .unwrap();
15801
15802        ensure_test_verification_ledger(&client, &resume.finalized_headers, 7, 8)
15803            .await
15804            .unwrap();
15805
15806        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15807            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15808        )))
15809        .fetch_one(&admin_pool)
15810        .await
15811        .unwrap();
15812        assert_eq!(nonce_state, (7, 0));
15813
15814        drop_execution_schema(&admin_pool, &schema).await;
15815    }
15816
15817    #[tokio::test]
15818    async fn verification_resume_rejects_unowned_nonce_advance() {
15819        for (test_name, mutation, observed_canonical_nonce) in [
15820            ("verification_resume_no_active_test", "no_active", 8),
15821            ("verification_resume_excess_test", "unchanged", 9),
15822            ("verification_resume_signed_test", "signed", 8),
15823            ("verification_resume_prepared_test", "prepared", 8),
15824            (
15825                "verification_resume_missing_payload_test",
15826                "missing_payload",
15827                8,
15828            ),
15829        ] {
15830            let Some((admin_pool, schema, client, _)) =
15831                execution_client_with_database(test_name, ready_rpc_state()).await
15832            else {
15833                return;
15834            };
15835            let database = client.cache.database.as_ref().unwrap();
15836            let (intent, _, _) =
15837                persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15838
15839            match mutation {
15840                "no_active" => {
15841                    sqlx::query(sqlx::AssertSqlSafe(format!(
15842                        "UPDATE {schema}.execution_intent SET active = FALSE WHERE id = $1"
15843                    )))
15844                    .bind(intent.id)
15845                    .execute(&admin_pool)
15846                    .await
15847                    .unwrap();
15848                }
15849                "signed" => {
15850                    sqlx::query(sqlx::AssertSqlSafe(format!(
15851                        "UPDATE {schema}.execution_intent SET status = 'signed' WHERE id = $1"
15852                    )))
15853                    .bind(intent.id)
15854                    .execute(&admin_pool)
15855                    .await
15856                    .unwrap();
15857                    sqlx::query(sqlx::AssertSqlSafe(format!(
15858                        "UPDATE {schema}.execution_transaction_hash SET status = 'signed' \
15859                         WHERE intent_id = $1"
15860                    )))
15861                    .bind(intent.id)
15862                    .execute(&admin_pool)
15863                    .await
15864                    .unwrap();
15865                }
15866                "prepared" => {
15867                    sqlx::query(sqlx::AssertSqlSafe(format!(
15868                        "UPDATE {schema}.execution_intent \
15869                         SET nonce = NULL, status = 'prepared' 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 current = FALSE \
15877                         WHERE intent_id = $1"
15878                    )))
15879                    .bind(intent.id)
15880                    .execute(&admin_pool)
15881                    .await
15882                    .unwrap();
15883                }
15884                "missing_payload" => {
15885                    sqlx::query(sqlx::AssertSqlSafe(format!(
15886                        "UPDATE {schema}.execution_transaction_hash \
15887                         SET payload_expected = FALSE, sealed_transaction = NULL \
15888                         WHERE intent_id = $1"
15889                    )))
15890                    .bind(intent.id)
15891                    .execute(&admin_pool)
15892                    .await
15893                    .unwrap();
15894                }
15895                "unchanged" => {}
15896                _ => unreachable!(),
15897            }
15898            let resume = database
15899                .load_execution_verification_resume(
15900                    42_161,
15901                    WALLET,
15902                    &client
15903                        .config
15904                        .verification
15905                        .as_ref()
15906                        .unwrap()
15907                        .manifest_version,
15908                    &client.config.verification.as_ref().unwrap().manifest_digest,
15909                )
15910                .await
15911                .unwrap()
15912                .unwrap();
15913
15914            let error = ensure_test_verification_ledger(
15915                &client,
15916                &resume.finalized_headers,
15917                7,
15918                observed_canonical_nonce,
15919            )
15920            .await
15921            .unwrap_err();
15922
15923            assert!(
15924                error
15925                    .to_string()
15926                    .contains(if observed_canonical_nonce == 9 {
15927                        "outside the owned recovery range"
15928                    } else {
15929                        "without"
15930                    }),
15931                "{mutation}: {error}"
15932            );
15933            let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15934                "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15935            )))
15936            .fetch_one(&admin_pool)
15937            .await
15938            .unwrap();
15939            assert_eq!(nonce_state, (7, 0), "{mutation}");
15940
15941            drop_execution_schema(&admin_pool, &schema).await;
15942        }
15943    }
15944
15945    #[tokio::test]
15946    async fn restart_marks_prepared_intent_recoverable_without_broadcast() {
15947        let Some((admin_pool, schema, client, state)) =
15948            execution_client_with_database("execution_prepared_restart_test", ready_rpc_state())
15949                .await
15950        else {
15951            return;
15952        };
15953        let database = client.cache.database.as_ref().unwrap();
15954        let intent = reserve_test_wrap_intent(database).await;
15955        *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Wrap));
15956
15957        client.reconcile_unresolved_execution().await.unwrap();
15958
15959        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15960            "SELECT status, active FROM {schema}.execution_intent WHERE id = {}",
15961            intent.id
15962        )))
15963        .fetch_one(&admin_pool)
15964        .await
15965        .unwrap();
15966        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15967            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
15968        )))
15969        .fetch_all(&admin_pool)
15970        .await
15971        .unwrap();
15972
15973        assert_eq!(status, "recoverable");
15974        assert!(!active);
15975        assert_eq!(transitions, ["prepared", "recoverable"]);
15976        assert!(client.in_flight.lock().is_none());
15977        assert!(
15978            state
15979                .recorded_requests()
15980                .iter()
15981                .all(|request| request["method"] != "eth_sendRawTransaction")
15982        );
15983
15984        drop_execution_schema(&admin_pool, &schema).await;
15985    }
15986
15987    #[tokio::test]
15988    async fn protected_restart_authenticates_envelope_before_recovery() {
15989        let initial_state = broadcast_rpc_state()
15990            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15991            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15992        let Some((admin_pool, schema, mut first_client, _)) =
15993            execution_client_with_database("protected_restart_test", initial_state).await
15994        else {
15995            return;
15996        };
15997        let database = first_client.cache.database.as_ref().unwrap().clone();
15998        let value = U256::from(1_000_000_000_000_000_u64);
15999        let expected_hash = expected_wrap_tx_hash(value).await;
16000
16001        let error = first_client.wrap(value).await.unwrap_err();
16002
16003        assert!(error.to_string().contains("Timed out awaiting finality"));
16004        let intent = database
16005            .get_active_execution_intent(42161, WALLET)
16006            .await
16007            .unwrap()
16008            .unwrap();
16009        let payload = database
16010            .get_execution_transaction_hashes(intent.id)
16011            .await
16012            .unwrap()
16013            .pop()
16014            .unwrap();
16015        assert!(payload.raw_transaction.is_none());
16016        assert!(payload.sealed_transaction.is_some());
16017        let keys = first_client.payload_keys.take().unwrap();
16018        drop(first_client);
16019
16020        let block = finalized_wrap_block(expected_hash);
16021        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16022        let restart_state = with_finalized_identity(
16023            execution_rpc_state()
16024                .with_response("eth_getTransactionReceipt", &receipt)
16025                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16026                .with_response_sequence(
16027                    "eth_call",
16028                    &[
16029                        CALL_BALANCE,
16030                        CALL_BALANCE,
16031                        CALL_BALANCE,
16032                        CALL_BALANCE_AFTER_WRAP,
16033                        CALL_BALANCE_AFTER_WRAP,
16034                        CALL_BALANCE_AFTER_WRAP,
16035                    ],
16036                ),
16037            &block,
16038            &receipt,
16039        );
16040        let addr = start_mock_rpc_server(restart_state.clone()).await;
16041        let mut restarted = test_client(format!("http://{addr}"));
16042        restarted.cache.database = Some(database);
16043        restarted.payload_keys = Some(keys);
16044        restarted.signer = Some(Arc::new(
16045            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16046        ));
16047
16048        restarted.reconcile_unresolved_execution().await.unwrap();
16049
16050        let record = restarted
16051            .cache
16052            .get_execution_transaction(42161, &expected_hash.to_string())
16053            .await
16054            .unwrap()
16055            .unwrap();
16056        let requests = restart_state.recorded_requests();
16057        assert_eq!(record.status, "finalized");
16058        assert!(restarted.in_flight.lock().is_none());
16059        assert_eq!(
16060            requests
16061                .iter()
16062                .filter(|request| request["method"] == "eth_sendRawTransaction")
16063                .count(),
16064            0
16065        );
16066
16067        drop_execution_schema(&admin_pool, &schema).await;
16068    }
16069
16070    #[tokio::test]
16071    async fn active_intent_reconciliation_waits_for_reservation_fence() {
16072        let Some((admin_pool, schema, client, _)) = execution_client_with_database(
16073            "execution_active_intent_reservation_fence_test",
16074            ready_rpc_state(),
16075        )
16076        .await
16077        else {
16078            return;
16079        };
16080        let database = client.cache.database.as_ref().unwrap().clone();
16081        let lock = PgAdvisoryLock::new(format!(
16082            "nautilus:blockchain:execution:42161:{}",
16083            WALLET.to_ascii_lowercase()
16084        ));
16085        let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
16086            unreachable!("string advisory locks use the 64-bit key space");
16087        };
16088        let mut reservation = admin_pool.begin().await.unwrap();
16089        sqlx::query("SELECT pg_advisory_xact_lock($1)")
16090            .bind(*lock_key)
16091            .execute(&mut *reservation)
16092            .await
16093            .unwrap();
16094        sqlx::query(sqlx::AssertSqlSafe(format!(
16095            "INSERT INTO {schema}.execution_intent (\
16096                schema_version, chain_id, wallet_address, purpose, status, transaction_to, \
16097                transaction_input, transaction_value, created_block\
16098             ) VALUES (2, $1, $2, 'wrap', 'prepared', $3, '0xd0e30db0', '1', $4)"
16099        )))
16100        .bind(42161_i32)
16101        .bind(WALLET)
16102        .bind(WETH_ADDRESS.to_string())
16103        .bind(i64::try_from(FIXTURE_BLOCK).unwrap())
16104        .execute(&mut *reservation)
16105        .await
16106        .unwrap();
16107
16108        let mut reconciliation = Box::pin(database.get_active_execution_intent(42161, WALLET));
16109        assert!(
16110            tokio::time::timeout(Duration::from_millis(100), &mut reconciliation)
16111                .await
16112                .is_err(),
16113            "reconciliation did not wait for the reservation fence"
16114        );
16115
16116        reservation.commit().await.unwrap();
16117        let intent = tokio::time::timeout(Duration::from_secs(2), reconciliation)
16118            .await
16119            .unwrap()
16120            .unwrap()
16121            .unwrap();
16122
16123        assert_eq!(intent.chain_id, 42161);
16124        assert_eq!(intent.wallet_address, WALLET);
16125        assert_eq!(intent.purpose, "wrap");
16126        assert_eq!(intent.status, "prepared");
16127        assert!(intent.active);
16128
16129        drop_execution_schema(&admin_pool, &schema).await;
16130    }
16131
16132    #[tokio::test]
16133    async fn restart_keeps_unbroadcast_signed_intent_reserved() {
16134        let Some((admin_pool, schema, client, state)) =
16135            execution_client_with_database("execution_signed_restart_test", ready_rpc_state())
16136                .await
16137        else {
16138            return;
16139        };
16140        let database = client.cache.database.as_ref().unwrap();
16141        let intent = reserve_test_wrap_intent(database).await;
16142        let transaction = build_eip1559_transaction(
16143            42161,
16144            7,
16145            78_000,
16146            130_000_000,
16147            10_000_000,
16148            WETH_ADDRESS,
16149            U256::from(1u64),
16150            Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
16151        );
16152        let (tx_hash, raw_transaction) = sign_eip1559_transaction(
16153            transaction,
16154            &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16155        )
16156        .await
16157        .unwrap();
16158        database
16159            .assign_execution_intent_nonce(intent.id, 7)
16160            .await
16161            .unwrap();
16162        let intent = database.get_execution_intent(intent.id).await.unwrap();
16163        persist_test_payload(
16164            database,
16165            client.payload_keys.as_deref(),
16166            &intent,
16167            tx_hash,
16168            &raw_transaction,
16169        )
16170        .await;
16171
16172        let error = client.reconcile_unresolved_execution().await.unwrap_err();
16173
16174        assert!(
16175            error
16176                .to_string()
16177                .contains("was not authorized for broadcast"),
16178            "was: {error}"
16179        );
16180        let recovery = recovering_in_flight(&client);
16181        assert_eq!(recovery.intent_id, intent.id);
16182        assert_eq!(recovery.nonce, 7);
16183        assert_eq!(recovery.purpose, TransactionPurpose::Wrap);
16184        assert_eq!(
16185            execution_intent_markers(&admin_pool, &schema).await,
16186            vec![("wrap".into(), "signed".into(), false, true)]
16187        );
16188        assert!(
16189            state
16190                .recorded_requests()
16191                .iter()
16192                .all(|request| request["method"] != "eth_sendRawTransaction")
16193        );
16194
16195        drop_execution_schema(&admin_pool, &schema).await;
16196    }
16197
16198    #[tokio::test]
16199    async fn restart_quarantines_legacy_recoverable_signed_intent() {
16200        let Some((admin_pool, schema, client, state)) = execution_client_with_database(
16201            "execution_legacy_recoverable_restart_test",
16202            ready_rpc_state(),
16203        )
16204        .await
16205        else {
16206            return;
16207        };
16208        let database = client.cache.database.as_ref().unwrap();
16209        let intent = reserve_test_wrap_intent(database).await;
16210        let tx_hash = B256::from([0x55; 32]);
16211        database
16212            .assign_execution_intent_nonce(intent.id, 7)
16213            .await
16214            .unwrap();
16215        let intent = database.get_execution_intent(intent.id).await.unwrap();
16216        persist_test_payload(
16217            database,
16218            client.payload_keys.as_deref(),
16219            &intent,
16220            tx_hash,
16221            &[0x01, 0x02, 0x03],
16222        )
16223        .await;
16224        sqlx::query(sqlx::AssertSqlSafe(format!(
16225            "UPDATE {schema}.execution_intent SET status = 'recoverable', active = FALSE WHERE id = {}",
16226            intent.id
16227        )))
16228        .execute(&admin_pool)
16229        .await
16230        .unwrap();
16231
16232        let error = client.reconcile_unresolved_execution().await.unwrap_err();
16233
16234        assert!(
16235            error
16236                .to_string()
16237                .contains("retains signed transaction bytes"),
16238            "was: {error}"
16239        );
16240        assert!(client.in_flight.lock().is_none());
16241        assert!(
16242            state
16243                .recorded_requests()
16244                .iter()
16245                .all(|request| request["method"] != "eth_sendRawTransaction")
16246        );
16247
16248        drop_execution_schema(&admin_pool, &schema).await;
16249    }
16250
16251    #[rstest]
16252    #[case::current("execution_invalid_signed_restart_test", false, "broadcast")]
16253    #[case::historical("execution_invalid_historical_restart_test", true, "replaced")]
16254    #[tokio::test]
16255    async fn restart_rejects_invalid_signed_bytes_before_recovery_effects(
16256        #[case] test_name: &str,
16257        #[case] historical: bool,
16258        #[case] expected_status: &str,
16259    ) {
16260        let Some((admin_pool, schema, first_client, state, _)) =
16261            swap_client_with_database(test_name, ready_rpc_state()).await
16262        else {
16263            return;
16264        };
16265        let database = first_client.cache.database.as_ref().unwrap().clone();
16266        let (intent, _) =
16267            persist_invalid_test_swap(&database, first_client.payload_keys.as_deref()).await;
16268
16269        if historical {
16270            sqlx::query(sqlx::AssertSqlSafe(format!(
16271                "UPDATE {schema}.execution_transaction_hash \
16272                 SET current = FALSE, status = 'replaced' WHERE intent_id = $1"
16273            )))
16274            .bind(intent.id)
16275            .execute(&admin_pool)
16276            .await
16277            .unwrap();
16278            sqlx::query(sqlx::AssertSqlSafe(format!(
16279                "INSERT INTO {schema}.execution_transaction_hash (\
16280                     intent_id, chain_id, transaction_hash, payload_expected, status, current\
16281                 ) VALUES ($1, 42161, $2, FALSE, 'replaced', TRUE)"
16282            )))
16283            .bind(intent.id)
16284            .bind(B256::from([0x44; 32]).to_string())
16285            .execute(&admin_pool)
16286            .await
16287            .unwrap();
16288            sqlx::query(sqlx::AssertSqlSafe(format!(
16289                "UPDATE {schema}.execution_intent SET status = 'replaced' WHERE id = $1"
16290            )))
16291            .bind(intent.id)
16292            .execute(&admin_pool)
16293            .await
16294            .unwrap();
16295        }
16296        let transitions_before: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16297            "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16298        )))
16299        .fetch_one(&admin_pool)
16300        .await
16301        .unwrap();
16302        let config = first_client.config.clone();
16303        let payload_keys = first_client.payload_keys.clone();
16304        drop(first_client);
16305
16306        let (mut restarted, _) = swap_client_with_cache(config);
16307        restarted.cache.database = Some(database);
16308        restarted.payload_keys = payload_keys;
16309        restarted.signer = Some(Arc::new(
16310            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16311        ));
16312        let mut receiver = start_with_events(&mut restarted);
16313
16314        let error = restarted
16315            .reconcile_unresolved_execution()
16316            .await
16317            .unwrap_err();
16318
16319        let recovery = recovering_in_flight(&restarted);
16320        let (status, acknowledgement_emitted, active): (String, bool, bool) =
16321            sqlx::query_as(sqlx::AssertSqlSafe(format!(
16322                "SELECT status, acknowledgement_emitted, active FROM {schema}.execution_intent"
16323            )))
16324            .fetch_one(&admin_pool)
16325            .await
16326            .unwrap();
16327        let transitions_after: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16328            "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16329        )))
16330        .fetch_one(&admin_pool)
16331        .await
16332        .unwrap();
16333
16334        assert!(
16335            error
16336                .to_string()
16337                .contains("is not a complete EIP-2718 envelope"),
16338            "was: {error}"
16339        );
16340        assert_eq!(recovery.intent_id, intent.id);
16341        assert_eq!(recovery.nonce, 7);
16342        assert_eq!(recovery.purpose, TransactionPurpose::Swap);
16343        assert_eq!(status, expected_status);
16344        assert!(!acknowledgement_emitted);
16345        assert!(active);
16346        assert_eq!(transitions_after, transitions_before);
16347        assert!(collect_order_events(&mut receiver).is_empty());
16348        assert!(
16349            state
16350                .recorded_requests()
16351                .iter()
16352                .all(|request| request["method"] != "eth_sendRawTransaction")
16353        );
16354
16355        drop_execution_schema(&admin_pool, &schema).await;
16356    }
16357
16358    #[tokio::test]
16359    async fn restart_rebroadcasts_only_durably_authorized_bytes() {
16360        let Some((admin_pool, schema, first_client, _)) =
16361            execution_client_with_database("execution_broadcast_restart_test", ready_rpc_state())
16362                .await
16363        else {
16364            return;
16365        };
16366        let database = first_client.cache.database.as_ref().unwrap().clone();
16367        let (_, _, raw_tx) =
16368            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16369        let payload_keys = first_client.payload_keys.clone();
16370        drop(first_client);
16371
16372        let restart_state = execution_rpc_state()
16373            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16374            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16375            .with_response("eth_call", CALL_EMPTY)
16376            .with_send_raw_transaction_echo();
16377        let addr = start_mock_rpc_server(restart_state.clone()).await;
16378        let mut restarted = test_client(format!("http://{addr}"));
16379        restarted.cache.database = Some(database);
16380        restarted.payload_keys = payload_keys;
16381        restarted.signer = Some(Arc::new(
16382            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16383        ));
16384
16385        restarted.reconcile_unresolved_execution().await.unwrap();
16386
16387        let broadcasts = restart_state
16388            .recorded_requests()
16389            .into_iter()
16390            .filter(|request| request["method"] == "eth_sendRawTransaction")
16391            .collect::<Vec<_>>();
16392        assert_eq!(broadcasts.len(), 1);
16393        assert_eq!(broadcasts[0]["params"][0], hex::encode_prefixed(&raw_tx));
16394        assert_eq!(
16395            execution_intent_markers(&admin_pool, &schema).await,
16396            vec![("wrap".into(), "dropped".into(), false, true)]
16397        );
16398
16399        drop_execution_schema(&admin_pool, &schema).await;
16400    }
16401
16402    #[tokio::test]
16403    async fn restart_suppresses_rebroadcast_when_canonical_nonce_advanced() {
16404        let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16405            "execution_rebroadcast_nonce_advanced_test",
16406            ready_rpc_state(),
16407        )
16408        .await
16409        else {
16410            return;
16411        };
16412        let database = first_client.cache.database.as_ref().unwrap().clone();
16413        let (intent, _, _) =
16414            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16415        let payload_keys = first_client.payload_keys.clone();
16416        drop(first_client);
16417
16418        let mut empty_block: serde_json::Value =
16419            serde_json::from_str(&replacement_head_block(B256::from([0x44; 32]))).unwrap();
16420        empty_block["result"]["transactions"] = serde_json::json!([]);
16421        let empty_block = empty_block.to_string();
16422        let restart_state = execution_rpc_state()
16423            .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16424            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16425            .with_response("eth_call", CALL_EMPTY)
16426            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &empty_block)
16427            .with_send_raw_transaction_echo();
16428        let addr = start_mock_rpc_server(restart_state.clone()).await;
16429        let mut restarted = test_client(format!("http://{addr}"));
16430        restarted.cache.database = Some(database);
16431        restarted.payload_keys = payload_keys;
16432        restarted.signer = Some(Arc::new(
16433            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16434        ));
16435
16436        let error = restarted
16437            .reconcile_unresolved_execution()
16438            .await
16439            .unwrap_err();
16440
16441        let requests = restart_state.recorded_requests();
16442        assert!(
16443            requests
16444                .iter()
16445                .all(|request| request["method"] != "eth_sendRawTransaction")
16446        );
16447        assert!(requests.iter().all(|request| {
16448            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16449        }));
16450        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16451            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16452             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16453        )))
16454        .bind(intent.id)
16455        .fetch_one(&admin_pool)
16456        .await
16457        .unwrap();
16458        assert!(
16459            error
16460                .to_string()
16461                .contains("without an authenticated signer transaction"),
16462            "was: {error}"
16463        );
16464        assert_eq!(decision_count, 6);
16465        let replacement_decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16466            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16467                 WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
16468        )))
16469        .bind(intent.id)
16470        .fetch_one(&admin_pool)
16471        .await
16472        .unwrap();
16473        assert_eq!(replacement_decision_count, 2);
16474        let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
16475            "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
16476        )))
16477        .fetch_one(&admin_pool)
16478        .await
16479        .unwrap();
16480        assert_eq!(nonce_state, (7, 0));
16481
16482        drop_execution_schema(&admin_pool, &schema).await;
16483    }
16484
16485    #[tokio::test]
16486    async fn restart_suppresses_rebroadcast_when_receipt_exists() {
16487        let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16488            "execution_rebroadcast_receipt_present_test",
16489            ready_rpc_state(),
16490        )
16491        .await
16492        else {
16493            return;
16494        };
16495        let database = first_client.cache.database.as_ref().unwrap().clone();
16496        let (intent, _, _) =
16497            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16498        let payload_keys = first_client.payload_keys.clone();
16499        drop(first_client);
16500
16501        let restart_state = execution_rpc_state()
16502            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16503            .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
16504            .with_response("eth_call", CALL_EMPTY)
16505            .with_send_raw_transaction_echo();
16506        let addr = start_mock_rpc_server(restart_state.clone()).await;
16507        let mut restarted = test_client(format!("http://{addr}"));
16508        restarted.cache.database = Some(database);
16509        restarted.payload_keys = payload_keys;
16510        restarted.signer = Some(Arc::new(
16511            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16512        ));
16513
16514        let error = restarted
16515            .reconcile_unresolved_execution()
16516            .await
16517            .unwrap_err();
16518
16519        assert!(
16520            error
16521                .to_string()
16522                .contains("finalized transaction verification is locally invalid"),
16523            "was: {error}"
16524        );
16525        let requests = restart_state.recorded_requests();
16526        assert!(
16527            requests
16528                .iter()
16529                .all(|request| request["method"] != "eth_sendRawTransaction")
16530        );
16531        assert!(requests.iter().all(|request| {
16532            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16533        }));
16534        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16535            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16536             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16537        )))
16538        .bind(intent.id)
16539        .fetch_one(&admin_pool)
16540        .await
16541        .unwrap();
16542        assert_eq!(decision_count, 6);
16543
16544        drop_execution_schema(&admin_pool, &schema).await;
16545    }
16546
16547    #[rstest]
16548    #[case("execution_rebroadcast_false_test", CALL_ZERO)]
16549    #[case("execution_rebroadcast_revert_test", CALL_REVERTED)]
16550    #[tokio::test]
16551    async fn restart_suppresses_rebroadcast_when_simulation_denies(
16552        #[case] test_name: &str,
16553        #[case] simulation_response: &str,
16554    ) {
16555        let Some((admin_pool, schema, first_client, _)) =
16556            execution_client_with_database(test_name, ready_rpc_state()).await
16557        else {
16558            return;
16559        };
16560        let database = first_client.cache.database.as_ref().unwrap().clone();
16561        let (intent, _, _) =
16562            persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16563        let payload_keys = first_client.payload_keys.clone();
16564        drop(first_client);
16565
16566        let restart_state = execution_rpc_state()
16567            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16568            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16569            .with_response("eth_call", simulation_response)
16570            .with_send_raw_transaction_echo();
16571        let addr = start_mock_rpc_server(restart_state.clone()).await;
16572        let mut restarted = test_client(format!("http://{addr}"));
16573        restarted.cache.database = Some(database);
16574        restarted.payload_keys = payload_keys;
16575        restarted.signer = Some(Arc::new(
16576            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16577        ));
16578
16579        restarted.reconcile_unresolved_execution().await.unwrap();
16580
16581        let requests = restart_state.recorded_requests();
16582        assert!(
16583            requests
16584                .iter()
16585                .all(|request| request["method"] != "eth_sendRawTransaction")
16586        );
16587        let simulation_calls = requests
16588            .iter()
16589            .filter(|request| {
16590                request["method"] == "eth_call"
16591                    && request["params"][0]["data"] == "0xd0e30db0"
16592                    && request["params"][1] == FIXTURE_BLOCK_PARAM
16593            })
16594            .count();
16595        assert_eq!(simulation_calls, 3);
16596        let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16597            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16598             WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16599        )))
16600        .bind(intent.id)
16601        .fetch_one(&admin_pool)
16602        .await
16603        .unwrap();
16604        assert_eq!(decision_count, 7);
16605
16606        drop_execution_schema(&admin_pool, &schema).await;
16607    }
16608
16609    #[tokio::test]
16610    async fn restart_wrap_identity_mismatch_keeps_signer_ownership() {
16611        let initial_state = broadcast_rpc_state()
16612            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16613            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16614        let Some((admin_pool, schema, mut first_client, _)) =
16615            execution_client_with_database("execution_restart_mismatch_test", initial_state).await
16616        else {
16617            return;
16618        };
16619        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16620        let error = first_client
16621            .wrap(U256::from(1_000_000_000_000_000_u64))
16622            .await
16623            .unwrap_err();
16624        assert!(
16625            error.to_string().contains("Timed out awaiting finality"),
16626            "was: {error}"
16627        );
16628        let database = first_client.cache.database.as_ref().unwrap().clone();
16629        let payload_keys = first_client.payload_keys.clone();
16630        drop(first_client);
16631
16632        // The finalized transaction carries no value, so it cannot be the persisted wrap
16633        let mismatched_block = serde_json::json!({
16634            "jsonrpc": "2.0",
16635            "id": 1,
16636            "result": {
16637                "number": "0x1cf0d41",
16638                "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
16639                "parentHash": FIXTURE_BLOCK_HASH,
16640                "timestamp": "0x69044a21",
16641                "baseFeePerGas": "0x5f5e100",
16642                "transactions": [{
16643                    "hash": expected_hash.to_string(),
16644                    "from": WALLET,
16645                    "nonce": "0x7",
16646                    "chainId": "0xa4b1",
16647                    "type": "0x2",
16648                    "to": WETH,
16649                    "input": "0xd0e30db0",
16650                    "value": "0x0",
16651                    "gas": "0x130b0",
16652                    "maxFeePerGas": "0x7bfa480",
16653                    "maxPriorityFeePerGas": "0x989680"
16654                }]
16655            }
16656        })
16657        .to_string();
16658        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16659        let restart_state = with_finalized_identity(
16660            execution_rpc_state()
16661                .with_response("eth_getTransactionReceipt", &receipt)
16662                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &mismatched_block),
16663            &mismatched_block,
16664            &receipt,
16665        );
16666        let addr = start_mock_rpc_server(restart_state.clone()).await;
16667        let mut restarted = test_client(format!("http://{addr}"));
16668        restarted.cache.database = Some(database);
16669        restarted.payload_keys = payload_keys;
16670        restarted.signer = Some(Arc::new(
16671            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16672        ));
16673
16674        let error = restarted
16675            .reconcile_unresolved_execution()
16676            .await
16677            .unwrap_err();
16678
16679        assert!(
16680            error
16681                .to_string()
16682                .contains("finalized transaction identity mismatch"),
16683            "was: {error}"
16684        );
16685        let in_flight = awaiting_in_flight(&restarted);
16686        assert_eq!(in_flight.nonce, 7);
16687        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16688        assert_eq!(in_flight.tx_hash, expected_hash);
16689        let requests = restart_state.recorded_requests();
16690        assert!(
16691            requests.iter().all(|request| {
16692                request["method"] != "eth_call"
16693                    || !request["params"][0]["data"]
16694                        .as_str()
16695                        .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
16696            }),
16697            "the postcondition must not run when call identity is unproven"
16698        );
16699        assert!(
16700            requests
16701                .iter()
16702                .all(|request| request["method"] != "eth_sendRawTransaction")
16703        );
16704        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16705            "SELECT status, active FROM {schema}.execution_intent"
16706        )))
16707        .fetch_one(&admin_pool)
16708        .await
16709        .unwrap();
16710        assert_eq!(status, "dropped");
16711        assert!(active);
16712
16713        let database = restarted.cache.database.as_ref().unwrap().clone();
16714        let payload_keys = restarted.payload_keys.clone();
16715        drop(restarted);
16716        let mut second = test_client(format!("http://{addr}"));
16717        second.cache.database = Some(database);
16718        second.payload_keys = payload_keys;
16719        second.signer = Some(Arc::new(
16720            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16721        ));
16722        let error = second.reconcile_unresolved_execution().await.unwrap_err();
16723        assert!(
16724            error
16725                .to_string()
16726                .contains("finalized transaction identity mismatch"),
16727            "was: {error}"
16728        );
16729        let active: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16730            "SELECT active FROM {schema}.execution_intent"
16731        )))
16732        .fetch_one(&admin_pool)
16733        .await
16734        .unwrap();
16735        assert!(active);
16736
16737        drop_execution_schema(&admin_pool, &schema).await;
16738    }
16739
16740    #[tokio::test]
16741    async fn restart_wrap_postcondition_failure_keeps_signer_ownership() {
16742        let initial_state = broadcast_rpc_state()
16743            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16744            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16745        let Some((admin_pool, schema, mut first_client, _)) =
16746            execution_client_with_database("execution_restart_postcondition_test", initial_state)
16747                .await
16748        else {
16749            return;
16750        };
16751        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16752        let error = first_client
16753            .wrap(U256::from(1_000_000_000_000_000_u64))
16754            .await
16755            .unwrap_err();
16756        assert!(
16757            error.to_string().contains("Timed out awaiting finality"),
16758            "was: {error}"
16759        );
16760        let database = first_client.cache.database.as_ref().unwrap().clone();
16761        let payload_keys = first_client.payload_keys.clone();
16762        drop(first_client);
16763
16764        // Call identity matches, but the wrapped balance does not increase
16765        let failing_wrap_state = || {
16766            let block = finalized_wrap_block(expected_hash);
16767            let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16768            with_finalized_identity(
16769                execution_rpc_state()
16770                    .with_response("eth_getTransactionReceipt", &receipt)
16771                    .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16772                    .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
16773                &block,
16774                &receipt,
16775            )
16776        };
16777        let restart_state = failing_wrap_state();
16778        let addr = start_mock_rpc_server(restart_state.clone()).await;
16779        let mut restarted = test_client(format!("http://{addr}"));
16780        restarted.cache.database = Some(database);
16781        restarted.payload_keys = payload_keys;
16782        restarted.signer = Some(Arc::new(
16783            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16784        ));
16785
16786        let error = restarted
16787            .reconcile_unresolved_execution()
16788            .await
16789            .unwrap_err();
16790
16791        assert!(
16792            error.to_string().contains("did not increase by"),
16793            "was: {error}"
16794        );
16795        let in_flight = awaiting_in_flight(&restarted);
16796        assert_eq!(in_flight.nonce, 7);
16797        assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16798        assert!(
16799            restart_state
16800                .recorded_requests()
16801                .iter()
16802                .all(|request| request["method"] != "eth_sendRawTransaction")
16803        );
16804        assert_eq!(
16805            execution_intent_markers(&admin_pool, &schema).await,
16806            vec![("wrap".into(), "dropped".into(), false, true)]
16807        );
16808
16809        let addr = start_mock_rpc_server(failing_wrap_state()).await;
16810        let error = later_reconnect(restarted, format!("http://{addr}")).await;
16811        assert!(
16812            error.to_string().contains("did not increase by"),
16813            "was: {error}"
16814        );
16815        assert_eq!(
16816            execution_intent_markers(&admin_pool, &schema).await,
16817            vec![("wrap".into(), "dropped".into(), false, true)]
16818        );
16819
16820        drop_execution_schema(&admin_pool, &schema).await;
16821    }
16822
16823    #[tokio::test]
16824    async fn restart_wrap_revert_marks_terminal_and_releases() {
16825        let initial_state = broadcast_rpc_state()
16826            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16827            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16828        let Some((admin_pool, schema, mut first_client, _)) =
16829            execution_client_with_database("execution_restart_wrap_revert_test", initial_state)
16830                .await
16831        else {
16832            return;
16833        };
16834        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16835        let error = first_client
16836            .wrap(U256::from(1_000_000_000_000_000_u64))
16837            .await
16838            .unwrap_err();
16839        assert!(
16840            error.to_string().contains("Timed out awaiting finality"),
16841            "was: {error}"
16842        );
16843        let database = first_client.cache.database.as_ref().unwrap().clone();
16844        let payload_keys = first_client.payload_keys.clone();
16845        drop(first_client);
16846
16847        let block = finalized_wrap_block(expected_hash);
16848        let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
16849        let restart_state = with_finalized_identity(
16850            execution_rpc_state()
16851                .with_response("eth_getTransactionReceipt", &receipt)
16852                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
16853            &block,
16854            &receipt,
16855        );
16856        let addr = start_mock_rpc_server(restart_state.clone()).await;
16857        let mut restarted = test_client(format!("http://{addr}"));
16858        restarted.cache.database = Some(database);
16859        restarted.payload_keys = payload_keys;
16860        restarted.signer = Some(Arc::new(
16861            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16862        ));
16863
16864        restarted.reconcile_unresolved_execution().await.unwrap();
16865
16866        assert!(restarted.in_flight.lock().is_none());
16867        assert!(
16868            restart_state
16869                .recorded_requests()
16870                .iter()
16871                .all(|request| request["method"] != "eth_sendRawTransaction")
16872        );
16873        assert_eq!(
16874            execution_intent_markers(&admin_pool, &schema).await,
16875            vec![("wrap".into(), "reverted".into(), true, false)]
16876        );
16877
16878        drop_execution_schema(&admin_pool, &schema).await;
16879    }
16880
16881    #[tokio::test]
16882    async fn restart_quarantines_unretained_same_nonce_wrap_replacement() {
16883        let initial_state = broadcast_rpc_state()
16884            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16885            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16886        let Some((admin_pool, schema, mut first_client, _)) =
16887            execution_client_with_database("execution_restart_replacement_test", initial_state)
16888                .await
16889        else {
16890            return;
16891        };
16892        let original_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16893        let error = first_client
16894            .wrap(U256::from(1_000_000_000_000_000_u64))
16895            .await
16896            .unwrap_err();
16897        assert!(
16898            error.to_string().contains("Timed out awaiting finality"),
16899            "was: {error}"
16900        );
16901        let database = first_client.cache.database.as_ref().unwrap().clone();
16902        let payload_keys = first_client.payload_keys.clone();
16903        drop(first_client);
16904
16905        // Matching call fields do not authenticate an unknown same-nonce payload.
16906        let replacement_hash = B256::from([0x44; 32]);
16907        let restart_state = execution_rpc_state()
16908            .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16909            .with_response_sequence("eth_call", &[CALL_BALANCE, CALL_BALANCE_AFTER_WRAP])
16910            .with_response_sequence(
16911                "eth_getTransactionReceipt",
16912                &[RECEIPT_NULL, RECEIPT_NULL, RECEIPT_NULL],
16913            )
16914            .with_parameter_response(
16915                "eth_getBlockByNumber",
16916                "0x1cf0d40",
16917                &replacement_head_block(replacement_hash),
16918            )
16919            .with_parameter_response(
16920                "eth_getBlockByNumber",
16921                "0x1cf0d41",
16922                &finalized_wrap_block(replacement_hash),
16923            );
16924        let addr = start_mock_rpc_server(restart_state.clone()).await;
16925        let mut restarted = test_client(format!("http://{addr}"));
16926        restarted.cache.database = Some(database);
16927        restarted.payload_keys = payload_keys;
16928        restarted.signer = Some(Arc::new(
16929            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16930        ));
16931        restarted.transaction_limits.receipt_timeout_secs = 2;
16932
16933        let error = restarted
16934            .reconcile_unresolved_execution()
16935            .await
16936            .unwrap_err();
16937
16938        let hashes: Vec<(String, String, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16939            "SELECT transaction_hash, status, current FROM \
16940                 {schema}.execution_transaction_hash ORDER BY id"
16941        )))
16942        .fetch_all(&admin_pool)
16943        .await
16944        .unwrap();
16945        let requests = restart_state.recorded_requests();
16946
16947        assert!(
16948            error
16949                .to_string()
16950                .contains("has no authenticated retained payload"),
16951            "was: {error}"
16952        );
16953        assert_eq!(
16954            hashes,
16955            [(original_hash.to_string(), "dropped".to_string(), true)]
16956        );
16957        assert!(restarted.in_flight.lock().is_some());
16958        assert_eq!(
16959            execution_intent_markers(&admin_pool, &schema).await,
16960            vec![("wrap".into(), "dropped".into(), false, true)]
16961        );
16962        assert_eq!(
16963            requests
16964                .iter()
16965                .filter(|request| request["method"] == "eth_getTransactionReceipt")
16966                .count(),
16967            3
16968        );
16969        assert!(requests.iter().all(|request| {
16970            request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16971        }));
16972        assert!(
16973            requests
16974                .iter()
16975                .all(|request| request["method"] != "eth_sendRawTransaction")
16976        );
16977
16978        drop_execution_schema(&admin_pool, &schema).await;
16979    }
16980
16981    #[tokio::test]
16982    async fn replacement_scan_accepts_only_an_authenticated_retained_payload() {
16983        let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
16984        let mut replacement_block: serde_json::Value =
16985            serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
16986        replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x1");
16987        let replacement_block = replacement_block.to_string();
16988        let state = execution_rpc_state().with_parameter_response(
16989            "eth_getBlockByNumber",
16990            "0x1cf0d40",
16991            &replacement_block,
16992        );
16993        let Some((admin_pool, schema, client, _)) =
16994            execution_client_with_database("replacement_scan_authenticated", state).await
16995        else {
16996            return;
16997        };
16998        let database = client.cache.database.as_ref().unwrap();
16999        let (intent, persisted_hash, persisted_raw) =
17000            persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
17001        assert_eq!(persisted_hash, expected_hash);
17002        let authenticated_payloads = HashMap::from([(expected_hash, persisted_raw.clone())]);
17003        let head = VerifiedBlockHeader {
17004            number: FIXTURE_BLOCK,
17005            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17006            parent_hash: B256::from_str(
17007                "0x0000000000000000000000000000000000000000000000000000000000000001",
17008            )
17009            .unwrap(),
17010            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17011            base_fee_per_gas: Some(100_000_000),
17012        };
17013
17014        let matched = client
17015            .transaction_executor()
17016            .unwrap()
17017            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17018            .await
17019            .unwrap()
17020            .unwrap();
17021
17022        assert_eq!(matched, (expected_hash, persisted_raw));
17023        let cursor: (i64, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17024            "SELECT finalized_cursor_number, finalized_cursor_hash \
17025             FROM {schema}.execution_replacement_scan WHERE intent_id = $1"
17026        )))
17027        .bind(intent.id)
17028        .fetch_one(&admin_pool)
17029        .await
17030        .unwrap();
17031        assert_eq!(
17032            cursor,
17033            (FIXTURE_BLOCK as i64, FIXTURE_BLOCK_HASH.to_string())
17034        );
17035        let evidence_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17036            "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
17037             WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
17038        )))
17039        .bind(intent.id)
17040        .fetch_one(&admin_pool)
17041        .await
17042        .unwrap();
17043        assert_eq!(evidence_count, 2);
17044
17045        drop_execution_schema(&admin_pool, &schema).await;
17046    }
17047
17048    #[tokio::test]
17049    async fn restart_reconciles_finalized_approve_after_validation() {
17050        let initial_state = broadcast_rpc_state()
17051            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
17052            .with_response("eth_call", CALL_BOOL_TRUE)
17053            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
17054            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
17055        let Some((admin_pool, schema, mut first_client, _)) =
17056            execution_client_with_database("execution_restart_approve_test", initial_state).await
17057        else {
17058            return;
17059        };
17060        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
17061        let error = first_client
17062            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17063            .await
17064            .unwrap_err();
17065        assert!(
17066            error.to_string().contains("Timed out awaiting finality"),
17067            "was: {error}"
17068        );
17069        let database = first_client.cache.database.as_ref().unwrap().clone();
17070        let payload_keys = first_client.payload_keys.clone();
17071        drop(first_client);
17072
17073        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
17074        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
17075        let restart_state = with_finalized_identity(
17076            execution_rpc_state()
17077                .with_response("eth_getTransactionReceipt", &receipt)
17078                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
17079                .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE_1000),
17080            &block,
17081            &receipt,
17082        );
17083        let addr = start_mock_rpc_server(restart_state.clone()).await;
17084        let mut restarted = test_client(format!("http://{addr}"));
17085        restarted.cache.database = Some(database);
17086        restarted.payload_keys = payload_keys;
17087        restarted.signer = Some(Arc::new(
17088            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
17089        ));
17090
17091        restarted.reconcile_unresolved_execution().await.unwrap();
17092
17093        let record = restarted
17094            .cache
17095            .get_execution_transaction(42161, &expected_hash.to_string())
17096            .await
17097            .unwrap()
17098            .unwrap();
17099        let requests = restart_state.recorded_requests();
17100        assert_eq!(record.status, "finalized");
17101        assert_eq!(record.purpose, "approve");
17102        assert!(restarted.in_flight.lock().is_none());
17103        assert_eq!(
17104            execution_intent_markers(&admin_pool, &schema).await,
17105            vec![("approve".into(), "finalized".into(), true, false)]
17106        );
17107        assert_eq!(
17108            requests
17109                .iter()
17110                .filter(|request| {
17111                    request["method"] == "eth_call"
17112                        && request["params"][0]["data"]
17113                            .as_str()
17114                            .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17115                })
17116                .count(),
17117            3
17118        );
17119        assert_eq!(
17120            requests
17121                .iter()
17122                .filter(|request| request["method"] == "eth_sendRawTransaction")
17123                .count(),
17124            0
17125        );
17126
17127        drop_execution_schema(&admin_pool, &schema).await;
17128    }
17129
17130    #[tokio::test]
17131    async fn restart_approve_postcondition_failure_keeps_signer_ownership() {
17132        let initial_state = broadcast_rpc_state()
17133            .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
17134            .with_response("eth_call", CALL_BOOL_TRUE)
17135            .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
17136            .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
17137        let Some((admin_pool, schema, mut first_client, _)) =
17138            execution_client_with_database("execution_restart_approve_post_test", initial_state)
17139                .await
17140        else {
17141            return;
17142        };
17143        let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
17144        let error = first_client
17145            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17146            .await
17147            .unwrap_err();
17148        assert!(
17149            error.to_string().contains("Timed out awaiting finality"),
17150            "was: {error}"
17151        );
17152        let database = first_client.cache.database.as_ref().unwrap().clone();
17153        let payload_keys = first_client.payload_keys.clone();
17154        drop(first_client);
17155
17156        // Call identity matches, but the allowance does not equal the approved amount
17157        let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
17158        let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
17159        let restart_state = with_finalized_identity(
17160            execution_rpc_state()
17161                .with_response("eth_getTransactionReceipt", &receipt)
17162                .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
17163                .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
17164            &block,
17165            &receipt,
17166        );
17167        let addr = start_mock_rpc_server(restart_state.clone()).await;
17168        let mut restarted = test_client(format!("http://{addr}"));
17169        restarted.cache.database = Some(database);
17170        restarted.payload_keys = payload_keys;
17171        restarted.signer = Some(Arc::new(
17172            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
17173        ));
17174
17175        let error = restarted
17176            .reconcile_unresolved_execution()
17177            .await
17178            .unwrap_err();
17179
17180        assert!(
17181            error
17182                .to_string()
17183                .contains("does not equal the requested amount"),
17184            "was: {error}"
17185        );
17186        let in_flight = awaiting_in_flight(&restarted);
17187        assert_eq!(in_flight.nonce, 7);
17188        assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
17189        assert!(
17190            restart_state
17191                .recorded_requests()
17192                .iter()
17193                .all(|request| request["method"] != "eth_sendRawTransaction")
17194        );
17195        assert_eq!(
17196            execution_intent_markers(&admin_pool, &schema).await,
17197            vec![("approve".into(), "dropped".into(), false, true)]
17198        );
17199
17200        let error = later_reconnect(restarted, format!("http://{addr}")).await;
17201        assert!(
17202            error
17203                .to_string()
17204                .contains("does not equal the requested amount"),
17205            "was: {error}"
17206        );
17207        assert_eq!(
17208            execution_intent_markers(&admin_pool, &schema).await,
17209            vec![("approve".into(), "dropped".into(), false, true)]
17210        );
17211
17212        drop_execution_schema(&admin_pool, &schema).await;
17213    }
17214
17215    #[tokio::test]
17216    async fn disappearing_unfinalized_receipt_drops_without_committing_inclusion() {
17217        let state = execution_rpc_state()
17218            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17219            .with_response("eth_estimateGas", ESTIMATE_GAS)
17220            .with_response("eth_call", CALL_BALANCE)
17221            .with_response_sequence(
17222                "eth_getTransactionReceipt",
17223                &[
17224                    RECEIPT_SUCCESS,
17225                    RECEIPT_SUCCESS,
17226                    RECEIPT_SUCCESS,
17227                    RECEIPT_NULL,
17228                    RECEIPT_NULL,
17229                    RECEIPT_NULL,
17230                ],
17231            )
17232            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17233            .with_send_raw_transaction_echo();
17234        let Some((admin_pool, schema, mut client, _)) =
17235            execution_client_with_database("execution_receipt_disappeared_test", state).await
17236        else {
17237            return;
17238        };
17239        client.transaction_limits.receipt_timeout_secs = 2;
17240
17241        let error = client
17242            .wrap(U256::from(1_000_000_000_000_000_u64))
17243            .await
17244            .unwrap_err();
17245        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17246            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17247        )))
17248        .fetch_all(&admin_pool)
17249        .await
17250        .unwrap();
17251
17252        assert!(
17253            error.to_string().contains("Timed out awaiting finality"),
17254            "was: {error}"
17255        );
17256        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17257        assert!(client.in_flight.lock().is_some());
17258
17259        drop_execution_schema(&admin_pool, &schema).await;
17260    }
17261
17262    #[tokio::test]
17263    async fn changed_unfinalized_block_drops_without_committing_inclusion() {
17264        let changed_block = serde_json::json!({
17265            "jsonrpc": "2.0",
17266            "id": 1,
17267            "result": {
17268                "number": "0x1cf0d41",
17269                "hash": "0x4444444444444444444444444444444444444444444444444444444444444444",
17270                "parentHash": FIXTURE_BLOCK_HASH,
17271                "timestamp": "0x69044a21",
17272                "baseFeePerGas": "0x5f5e100",
17273                "transactions": []
17274            }
17275        })
17276        .to_string();
17277        let state = execution_rpc_state()
17278            .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17279            .with_response("eth_estimateGas", ESTIMATE_GAS)
17280            .with_response("eth_call", CALL_BALANCE)
17281            .with_response_sequence(
17282                "eth_getTransactionReceipt",
17283                &[
17284                    RECEIPT_SUCCESS,
17285                    RECEIPT_SUCCESS,
17286                    RECEIPT_SUCCESS,
17287                    RECEIPT_SUCCESS,
17288                    RECEIPT_SUCCESS,
17289                    RECEIPT_SUCCESS,
17290                ],
17291            )
17292            .with_parameter_response_sequence(
17293                "eth_getBlockByNumber",
17294                "0x1cf0d41",
17295                &[
17296                    BLOCK_CANONICAL,
17297                    BLOCK_CANONICAL,
17298                    BLOCK_CANONICAL,
17299                    &changed_block,
17300                    &changed_block,
17301                    &changed_block,
17302                ],
17303            )
17304            .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17305            .with_send_raw_transaction_echo();
17306        let Some((admin_pool, schema, mut client, _)) =
17307            execution_client_with_database("execution_reorg_test", state).await
17308        else {
17309            return;
17310        };
17311        client.transaction_limits.receipt_timeout_secs = 2;
17312
17313        let error = client
17314            .wrap(U256::from(1_000_000_000_000_000_u64))
17315            .await
17316            .unwrap_err();
17317        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17318            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17319        )))
17320        .fetch_all(&admin_pool)
17321        .await
17322        .unwrap();
17323
17324        assert!(
17325            error.to_string().contains("Timed out awaiting finality"),
17326            "was: {error}"
17327        );
17328        assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17329        assert!(client.in_flight.lock().is_some());
17330
17331        drop_execution_schema(&admin_pool, &schema).await;
17332    }
17333
17334    #[tokio::test]
17335    async fn pre_sign_pending_nonce_drift_blocks_wrap_before_signature() {
17336        let state = execution_rpc_state()
17337            .with_response_sequence(
17338                "eth_getTransactionCount",
17339                &[
17340                    TRANSACTION_COUNT,
17341                    TRANSACTION_COUNT,
17342                    TRANSACTION_COUNT,
17343                    TRANSACTION_COUNT_NEXT,
17344                    TRANSACTION_COUNT_NEXT,
17345                    TRANSACTION_COUNT_NEXT,
17346                ],
17347            )
17348            .with_response("eth_estimateGas", ESTIMATE_GAS)
17349            .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
17350            .with_send_raw_transaction_echo();
17351        let Some((admin_pool, schema, mut client, state)) =
17352            execution_client_with_database("pre_sign_pending_nonce_drift", state).await
17353        else {
17354            return;
17355        };
17356
17357        let error = client
17358            .wrap(U256::from(1_000_000_000_000_000_u64))
17359            .await
17360            .unwrap_err();
17361        let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17362            "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
17363        )))
17364        .fetch_one(&admin_pool)
17365        .await
17366        .unwrap();
17367
17368        assert!(
17369            error
17370                .to_string()
17371                .contains("Pending nonce does not match the verified canonical nonce"),
17372            "was: {error}"
17373        );
17374        assert_eq!(signed_count, 0);
17375        assert!(
17376            state
17377                .recorded_requests()
17378                .iter()
17379                .all(|request| request["method"] != "eth_sendRawTransaction")
17380        );
17381        assert!(client.in_flight.lock().is_none());
17382
17383        drop_execution_schema(&admin_pool, &schema).await;
17384    }
17385
17386    #[tokio::test]
17387    async fn unknown_same_nonce_swap_replacement_emits_no_rejection() {
17388        let replacement_hash = B256::from([0x44; 32]);
17389        let replacement_block = replacement_head_block(replacement_hash);
17390        let state = execution_rpc_state()
17391            .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &replacement_block)
17392            .with_send_raw_transaction_echo();
17393        let Some((admin_pool, schema, mut client, _, _)) =
17394            swap_client_with_database("unknown_same_nonce_swap_replacement", state).await
17395        else {
17396            return;
17397        };
17398        let mut receiver = start_with_events(&mut client);
17399        let database = client.cache.database.as_ref().unwrap();
17400        let (intent, original_hash, original_payload) =
17401            persist_test_swap_broadcast(database, client.payload_keys.as_deref()).await;
17402        let authenticated_payloads = HashMap::from([(original_hash, original_payload)]);
17403        let head = VerifiedBlockHeader {
17404            number: FIXTURE_BLOCK,
17405            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17406            parent_hash: B256::from_str(
17407                "0x0000000000000000000000000000000000000000000000000000000000000001",
17408            )
17409            .unwrap(),
17410            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17411            base_fee_per_gas: Some(100_000_000),
17412        };
17413
17414        let error = client
17415            .transaction_executor()
17416            .unwrap()
17417            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17418            .await
17419            .unwrap_err();
17420        let events = collect_order_events(&mut receiver);
17421        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17422            "SELECT status, active FROM {schema}.execution_intent"
17423        )))
17424        .fetch_one(&admin_pool)
17425        .await
17426        .unwrap();
17427
17428        assert!(
17429            error
17430                .to_string()
17431                .contains("has no authenticated retained payload"),
17432            "was: {error}"
17433        );
17434        assert!(events.is_empty(), "was: {events:?}");
17435        assert_eq!(status, "broadcast");
17436        assert!(active);
17437
17438        drop_execution_schema(&admin_pool, &schema).await;
17439    }
17440
17441    #[tokio::test]
17442    async fn replacement_scan_rejects_rpc_fields_that_conflict_with_the_payload() {
17443        let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
17444        let mut replacement_block: serde_json::Value =
17445            serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
17446        replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x2");
17447        let replacement_block = replacement_block.to_string();
17448        let state = execution_rpc_state().with_parameter_response(
17449            "eth_getBlockByNumber",
17450            "0x1cf0d40",
17451            &replacement_block,
17452        );
17453        let Some((admin_pool, schema, client, _)) =
17454            execution_client_with_database("replacement_scan_payload_mismatch", state).await
17455        else {
17456            return;
17457        };
17458        let database = client.cache.database.as_ref().unwrap();
17459        let (intent, persisted_hash, persisted_payload) =
17460            persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
17461        assert_eq!(persisted_hash, expected_hash);
17462        let authenticated_payloads = HashMap::from([(persisted_hash, persisted_payload)]);
17463        let head = VerifiedBlockHeader {
17464            number: FIXTURE_BLOCK,
17465            hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17466            parent_hash: B256::from_str(
17467                "0x0000000000000000000000000000000000000000000000000000000000000001",
17468            )
17469            .unwrap(),
17470            timestamp: FIXTURE_BLOCK_TIMESTAMP,
17471            base_fee_per_gas: Some(100_000_000),
17472        };
17473
17474        let error = client
17475            .transaction_executor()
17476            .unwrap()
17477            .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17478            .await
17479            .unwrap_err();
17480        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17481            "SELECT status, active FROM {schema}.execution_intent"
17482        )))
17483        .fetch_one(&admin_pool)
17484        .await
17485        .unwrap();
17486
17487        assert!(
17488            error
17489                .to_string()
17490                .contains("failed authenticated payload validation"),
17491            "was: {error}"
17492        );
17493        assert_eq!(status, "broadcast");
17494        assert!(active);
17495
17496        drop_execution_schema(&admin_pool, &schema).await;
17497    }
17498
17499    #[tokio::test]
17500    async fn execution_transaction_constraints_reject_conflicting_identity() {
17501        const TRANSACTION_HASH: &str = "0xduplicate-transaction-hash";
17502        const OTHER_WALLET: &str = "0x0000000000000000000000000000000000000001";
17503        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17504            "execution_duplicate_record_test",
17505            ready_rpc_state(),
17506        )
17507        .await
17508        else {
17509            return;
17510        };
17511        let database = client.cache.database.as_ref().unwrap();
17512        let operator = database
17513            .reserve_execution_intent(&ExecutionIntentInsert {
17514                chain_id: 42161,
17515                wallet_address: WALLET.to_string(),
17516                purpose: "wrap".to_string(),
17517                client_order_id: None,
17518                trader_id: None,
17519                strategy_id: None,
17520                account_id: None,
17521                instrument_id: None,
17522                pool_address: None,
17523                transaction_to: WETH_ADDRESS.to_string(),
17524                transaction_input: "0xd0e30db0".to_string(),
17525                transaction_value: "1".to_string(),
17526                amount_in: None,
17527                created_block: FIXTURE_BLOCK,
17528            })
17529            .await
17530            .unwrap();
17531        database
17532            .assign_execution_intent_nonce(operator.id, 7)
17533            .await
17534            .unwrap();
17535        database
17536            .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17537            .await
17538            .unwrap();
17539        database
17540            .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17541            .await
17542            .unwrap();
17543
17544        let signer_conflict = database
17545            .reserve_execution_intent(&ExecutionIntentInsert {
17546                chain_id: 42161,
17547                wallet_address: WALLET.to_string(),
17548                purpose: "approve".to_string(),
17549                client_order_id: None,
17550                trader_id: None,
17551                strategy_id: None,
17552                account_id: None,
17553                instrument_id: None,
17554                pool_address: None,
17555                transaction_to: ROUTER_ADDRESS.to_string(),
17556                transaction_input: "0x01".to_string(),
17557                transaction_value: "0".to_string(),
17558                amount_in: None,
17559                created_block: FIXTURE_BLOCK,
17560            })
17561            .await
17562            .unwrap_err();
17563        let other = database
17564            .reserve_execution_intent(&ExecutionIntentInsert {
17565                chain_id: 42161,
17566                wallet_address: OTHER_WALLET.to_string(),
17567                purpose: "approve".to_string(),
17568                client_order_id: None,
17569                trader_id: None,
17570                strategy_id: None,
17571                account_id: None,
17572                instrument_id: None,
17573                pool_address: None,
17574                transaction_to: ROUTER_ADDRESS.to_string(),
17575                transaction_input: "0x01".to_string(),
17576                transaction_value: "0".to_string(),
17577                amount_in: None,
17578                created_block: FIXTURE_BLOCK,
17579            })
17580            .await
17581            .unwrap();
17582        database
17583            .assign_execution_intent_nonce(other.id, 7)
17584            .await
17585            .unwrap();
17586        let hash_conflict = database
17587            .add_execution_transaction_hash(other.id, 42161, TRANSACTION_HASH, &[4, 5, 6])
17588            .await
17589            .unwrap_err();
17590
17591        let record = database
17592            .get_execution_transaction(42161, TRANSACTION_HASH)
17593            .await
17594            .unwrap()
17595            .unwrap();
17596        let count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17597            "SELECT COUNT(*) FROM {schema}.execution_intent"
17598        )))
17599        .fetch_one(&admin_pool)
17600        .await
17601        .unwrap();
17602
17603        assert_eq!(record.wallet_address.as_deref(), Some(WALLET));
17604        assert_eq!(record.nonce, 7);
17605        assert_eq!(record.transaction_hash, TRANSACTION_HASH);
17606        assert_eq!(record.purpose, "wrap");
17607        assert_eq!(record.status, "signed");
17608        assert_eq!(record.client_order_id, None);
17609        assert!(
17610            hash_conflict
17611                .to_string()
17612                .contains("conflicts with its persisted identity"),
17613            "was: {hash_conflict}"
17614        );
17615        assert_eq!(
17616            signer_conflict.to_string(),
17617            "Execution intent reservation failed before commit"
17618        );
17619        assert!(
17620            signer_conflict.chain().any(|cause| cause
17621                .to_string()
17622                .contains("execution_intent_active_signer_key")),
17623            "was: {signer_conflict:#}"
17624        );
17625        assert_eq!(count, 2);
17626
17627        drop_execution_schema(&admin_pool, &schema).await;
17628    }
17629
17630    #[tokio::test]
17631    async fn execution_status_transitions_are_idempotent() {
17632        const TRANSACTION_HASH: &str =
17633            "0x5555555555555555555555555555555555555555555555555555555555555555";
17634        let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17635            "execution_transition_test",
17636            ready_rpc_state(),
17637        )
17638        .await
17639        else {
17640            return;
17641        };
17642        let database = client.cache.database.as_ref().unwrap();
17643        let intent = database
17644            .reserve_execution_intent(&ExecutionIntentInsert {
17645                chain_id: 42161,
17646                wallet_address: WALLET.to_string(),
17647                purpose: "wrap".to_string(),
17648                client_order_id: None,
17649                trader_id: None,
17650                strategy_id: None,
17651                account_id: None,
17652                instrument_id: None,
17653                pool_address: None,
17654                transaction_to: WETH_ADDRESS.to_string(),
17655                transaction_input: "0xd0e30db0".to_string(),
17656                transaction_value: "1".to_string(),
17657                amount_in: None,
17658                created_block: FIXTURE_BLOCK,
17659            })
17660            .await
17661            .unwrap();
17662        database
17663            .assign_execution_intent_nonce(intent.id, 7)
17664            .await
17665            .unwrap();
17666        database
17667            .add_execution_transaction_hash(intent.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17668            .await
17669            .unwrap();
17670
17671        for _ in 0..2 {
17672            database
17673                .record_execution_status(
17674                    intent.id,
17675                    TRANSACTION_HASH,
17676                    TransactionStatus::Broadcast,
17677                    None,
17678                    None,
17679                    None,
17680                    None,
17681                    None,
17682                )
17683                .await
17684                .unwrap();
17685        }
17686
17687        for status in [TransactionStatus::Included, TransactionStatus::Included] {
17688            database
17689                .record_execution_status(
17690                    intent.id,
17691                    TRANSACTION_HASH,
17692                    status,
17693                    Some(FIXTURE_BLOCK + 1),
17694                    Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17695                    Some(true),
17696                    Some(50_112),
17697                    Some("100000000"),
17698                )
17699                .await
17700                .unwrap();
17701        }
17702
17703        for _ in 0..2 {
17704            database
17705                .record_execution_status(
17706                    intent.id,
17707                    TRANSACTION_HASH,
17708                    TransactionStatus::Finalized,
17709                    Some(FIXTURE_BLOCK + 1),
17710                    Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17711                    Some(true),
17712                    Some(50_112),
17713                    Some("100000000"),
17714                )
17715                .await
17716                .unwrap();
17717        }
17718
17719        let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17720            "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17721        )))
17722        .fetch_all(&admin_pool)
17723        .await
17724        .unwrap();
17725        let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17726            "SELECT status, active FROM {schema}.execution_intent"
17727        )))
17728        .fetch_one(&admin_pool)
17729        .await
17730        .unwrap();
17731        let append_only_error = sqlx::query(sqlx::AssertSqlSafe(format!(
17732            "DELETE FROM {schema}.execution_transaction_transition WHERE intent_id = {}",
17733            intent.id
17734        )))
17735        .execute(&admin_pool)
17736        .await
17737        .unwrap_err();
17738
17739        assert_eq!(
17740            transitions,
17741            ["prepared", "signed", "broadcast", "included", "finalized"]
17742        );
17743        assert_eq!(status, "finalized");
17744        assert!(active);
17745        assert!(
17746            append_only_error
17747                .to_string()
17748                .contains("Execution transitions are append-only"),
17749            "was: {append_only_error}"
17750        );
17751
17752        drop_execution_schema(&admin_pool, &schema).await;
17753    }
17754
17755    #[tokio::test]
17756    async fn wrap_then_approve_persists_records_and_clears_in_flight() {
17757        let state = execution_rpc_state()
17758            .with_parameter_response_sequence(
17759                "eth_getBlockByNumber",
17760                "latest",
17761                &[
17762                    BLOCK_BY_NUMBER,
17763                    BLOCK_BY_NUMBER,
17764                    BLOCK_BY_NUMBER,
17765                    BLOCK_FINALIZED,
17766                    BLOCK_FINALIZED,
17767                    BLOCK_FINALIZED,
17768                ],
17769            )
17770            .with_response_sequence(
17771                "eth_call",
17772                &[
17773                    CALL_BALANCE,
17774                    CALL_BALANCE,
17775                    CALL_BALANCE,
17776                    CALL_BALANCE,
17777                    CALL_BALANCE,
17778                    CALL_BALANCE,
17779                    CALL_BALANCE_AFTER_WRAP,
17780                    CALL_BALANCE_AFTER_WRAP,
17781                    CALL_BALANCE_AFTER_WRAP,
17782                    CALL_BOOL_TRUE,
17783                    CALL_BOOL_TRUE,
17784                    CALL_BOOL_TRUE,
17785                ],
17786            )
17787            .with_call_response_sequence(
17788                ALLOWANCE_SELECTOR,
17789                &[
17790                    CALL_ZERO,
17791                    CALL_ZERO,
17792                    CALL_ZERO,
17793                    CALL_ALLOWANCE_MAX,
17794                    CALL_ALLOWANCE_MAX,
17795                    CALL_ALLOWANCE_MAX,
17796                ],
17797            )
17798            .with_response_sequence(
17799                "eth_getTransactionCount",
17800                &[
17801                    TRANSACTION_COUNT,
17802                    TRANSACTION_COUNT,
17803                    TRANSACTION_COUNT,
17804                    TRANSACTION_COUNT,
17805                    TRANSACTION_COUNT,
17806                    TRANSACTION_COUNT,
17807                    TRANSACTION_COUNT_NEXT,
17808                    TRANSACTION_COUNT_NEXT,
17809                    TRANSACTION_COUNT_NEXT,
17810                    TRANSACTION_COUNT_NEXT,
17811                    TRANSACTION_COUNT_NEXT,
17812                    TRANSACTION_COUNT_NEXT,
17813                ],
17814            )
17815            .with_response("eth_estimateGas", ESTIMATE_GAS)
17816            .with_response_sequence(
17817                "eth_getTransactionReceipt",
17818                &[
17819                    RECEIPT_SUCCESS,
17820                    RECEIPT_SUCCESS,
17821                    RECEIPT_SUCCESS,
17822                    RECEIPT_SUCCESS,
17823                    RECEIPT_SUCCESS,
17824                    RECEIPT_SUCCESS,
17825                ],
17826            )
17827            .with_send_raw_transaction_echo();
17828        let Some((admin_pool, schema, mut client, state)) =
17829            execution_client_with_database("execution_client_test", state).await
17830        else {
17831            return;
17832        };
17833        client.config.unlimited_approval = true;
17834        client.transaction_limits.receipt_timeout_secs = 3;
17835
17836        let wrap_hash = client
17837            .wrap(U256::from(1_000_000_000_000_000u64))
17838            .await
17839            .unwrap();
17840
17841        let record = client
17842            .cache
17843            .get_execution_transaction(42161, &wrap_hash.to_string())
17844            .await
17845            .unwrap()
17846            .unwrap();
17847        assert_eq!(record.nonce, 7);
17848        assert_eq!(record.purpose, "wrap");
17849        assert_eq!(record.status, "finalized");
17850        assert_eq!(
17851            execution_intent_markers(&admin_pool, &schema).await,
17852            vec![("wrap".into(), "finalized".into(), true, false)]
17853        );
17854
17855        let approve_hash = client
17856            .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17857            .await
17858            .unwrap();
17859
17860        let record = client
17861            .cache
17862            .get_execution_transaction(42161, &approve_hash.to_string())
17863            .await
17864            .unwrap()
17865            .unwrap();
17866        assert_eq!(record.nonce, 8);
17867        assert_eq!(record.purpose, "approve");
17868        assert_eq!(record.status, "finalized");
17869        assert_eq!(
17870            execution_intent_markers(&admin_pool, &schema).await,
17871            vec![
17872                ("wrap".into(), "finalized".into(), true, false),
17873                ("approve".into(), "finalized".into(), true, false),
17874            ]
17875        );
17876
17877        let requests = state.recorded_requests();
17878        let broadcasts: Vec<_> = requests
17879            .iter()
17880            .filter(|request| request["method"] == "eth_sendRawTransaction")
17881            .collect();
17882        assert_eq!(broadcasts.len(), 2);
17883        for broadcast in &broadcasts {
17884            let payload = broadcast["params"][0].as_str().unwrap();
17885            assert!(payload.starts_with("0x02"), "was: {payload}");
17886        }
17887        let broadcast_indexes = requests
17888            .iter()
17889            .enumerate()
17890            .filter_map(|(index, request)| {
17891                (request["method"] == "eth_sendRawTransaction").then_some(index)
17892            })
17893            .collect::<Vec<_>>();
17894
17895        for (index, expected_block) in broadcast_indexes
17896            .into_iter()
17897            .zip([FIXTURE_BLOCK_PARAM, "0x1cf0d42"])
17898        {
17899            let nonce_fence = &requests[index - 6..index];
17900            assert!(nonce_fence[..3].iter().all(|request| {
17901                request["method"] == "eth_getTransactionCount"
17902                    && request["params"][1] == expected_block
17903            }));
17904            assert!(nonce_fence[3..].iter().all(|request| {
17905                request["method"] == "eth_getTransactionCount" && request["params"][1] == "pending"
17906            }));
17907        }
17908        let receipt_polls = requests
17909            .iter()
17910            .filter(|request| request["method"] == "eth_getTransactionReceipt")
17911            .count();
17912        assert_eq!(receipt_polls, 6);
17913        let allowance_calls: Vec<_> = requests
17914            .iter()
17915            .filter(|request| {
17916                request["method"] == "eth_call"
17917                    && request["params"][0]["data"]
17918                        .as_str()
17919                        .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17920            })
17921            .collect();
17922        assert_eq!(allowance_calls.len(), 6);
17923        assert!(
17924            allowance_calls[..3]
17925                .iter()
17926                .all(|request| request["params"][1] == "0x1cf0d42")
17927        );
17928        assert!(
17929            allowance_calls[3..]
17930                .iter()
17931                .all(|request| request["params"][1] == "0x1cf0d41")
17932        );
17933
17934        // Unlimited approval policy encoded U256::MAX in the approve calldata
17935        let estimates: Vec<_> = requests
17936            .iter()
17937            .filter(|request| request["method"] == "eth_estimateGas")
17938            .collect();
17939        assert_eq!(estimates.len(), 6);
17940        assert!(estimates[..3].iter().all(|request| {
17941            request["params"].as_array().unwrap().len() == 2
17942                && request["params"][1] == FIXTURE_BLOCK_PARAM
17943        }));
17944        assert!(estimates[3..].iter().all(|request| {
17945            request["params"].as_array().unwrap().len() == 2 && request["params"][1] == "0x1cf0d42"
17946        }));
17947        let approve_data = estimates[3]["params"][0]["data"].as_str().unwrap();
17948        assert!(
17949            approve_data.starts_with("0x095ea7b3"),
17950            "was: {approve_data}"
17951        );
17952        assert!(
17953            approve_data.ends_with(&"f".repeat(64)),
17954            "was: {approve_data}"
17955        );
17956
17957        for selector in [FACTORY_SELECTOR, WETH9_SELECTOR] {
17958            let calls: Vec<_> = requests
17959                .iter()
17960                .filter(|request| {
17961                    request["method"] == "eth_call"
17962                        && request["params"][0]["data"]
17963                            .as_str()
17964                            .is_some_and(|data| data.starts_with(selector))
17965                })
17966                .collect();
17967            assert!(!calls.is_empty(), "selector {selector}");
17968            assert!(
17969                calls.iter().all(|request| request["params"][1]
17970                    .as_str()
17971                    .is_some_and(|block| block.starts_with("0x"))),
17972                "selector {selector}: {calls:?}"
17973            );
17974        }
17975        let latest_blocks = requests
17976            .iter()
17977            .filter(|request| {
17978                request["method"] == "eth_getBlockByNumber"
17979                    && request["params"] == serde_json::json!(["latest", false])
17980            })
17981            .count();
17982        assert_eq!(latest_blocks, 6);
17983
17984        drop_execution_schema(&admin_pool, &schema).await;
17985    }
17986
17987    #[tokio::test]
17988    async fn reverted_receipt_marks_record_reverted_and_errors() {
17989        let state = signing_rpc_state()
17990            .with_response("eth_getTransactionReceipt", RECEIPT_REVERTED)
17991            .with_send_raw_transaction_echo();
17992        let Some((admin_pool, schema, mut client, _)) =
17993            execution_client_with_database("execution_reverted_test", state).await
17994        else {
17995            return;
17996        };
17997
17998        let error = client
17999            .wrap(U256::from(1_000_000_000_000_000u64))
18000            .await
18001            .unwrap_err();
18002
18003        assert!(
18004            error.to_string().contains("reverted on-chain"),
18005            "was: {error}"
18006        );
18007        assert!(client.in_flight.lock().is_none());
18008
18009        let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
18010
18011        let record = client
18012            .cache
18013            .get_execution_transaction(42161, &expected_hash.to_string())
18014            .await
18015            .unwrap()
18016            .unwrap();
18017        assert_eq!(record.status, "reverted");
18018        assert_eq!(
18019            execution_intent_markers(&admin_pool, &schema).await,
18020            vec![("wrap".into(), "reverted".into(), true, false)]
18021        );
18022
18023        drop_execution_schema(&admin_pool, &schema).await;
18024    }
18025
18026    fn market_sell_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
18027        OrderTestBuilder::new(OrderType::Market)
18028            .trader_id(TraderId::from("TRADER-001"))
18029            .strategy_id(StrategyId::from("S-001"))
18030            .instrument_id(instrument_id)
18031            .client_order_id(ClientOrderId::from(client_order_id))
18032            .side(OrderSide::Sell)
18033            .quantity(Quantity::from("0.001"))
18034            .build()
18035    }
18036
18037    fn market_buy_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
18038        OrderTestBuilder::new(OrderType::Market)
18039            .trader_id(TraderId::from("TRADER-001"))
18040            .strategy_id(StrategyId::from("S-001"))
18041            .instrument_id(instrument_id)
18042            .client_order_id(ClientOrderId::from(client_order_id))
18043            .side(OrderSide::Buy)
18044            .quantity(Quantity::from("0.001"))
18045            .build()
18046    }
18047
18048    fn submit_order_list_cmd(orders: &[OrderAny]) -> SubmitOrderList {
18049        let order_list = OrderList::new(
18050            OrderListId::from("OL-001"),
18051            orders[0].instrument_id(),
18052            orders[0].strategy_id(),
18053            orders.iter().map(|order| order.client_order_id()).collect(),
18054            UnixNanos::default(),
18055        );
18056        SubmitOrderList::new(
18057            TraderId::from("TRADER-001"),
18058            Some(ClientId::from("BLOCKCHAIN-001")),
18059            orders[0].strategy_id(),
18060            order_list,
18061            orders
18062                .iter()
18063                .map(|order| order.init_event().clone())
18064                .collect(),
18065            None,
18066            None,
18067            None,
18068            UUID4::new(),
18069            UnixNanos::default(),
18070            None,
18071        )
18072    }
18073
18074    fn modify_order_cmd(
18075        instrument_id: InstrumentId,
18076        client_order_id: ClientOrderId,
18077    ) -> ModifyOrder {
18078        ModifyOrder::new(
18079            TraderId::from("TRADER-001"),
18080            Some(ClientId::from("BLOCKCHAIN-001")),
18081            StrategyId::from("S-001"),
18082            instrument_id,
18083            client_order_id,
18084            None,
18085            Some(Quantity::from("0.002")),
18086            None,
18087            None,
18088            UUID4::new(),
18089            UnixNanos::default(),
18090            None,
18091            None,
18092        )
18093    }
18094
18095    fn cancel_order_cmd(
18096        instrument_id: InstrumentId,
18097        client_order_id: ClientOrderId,
18098    ) -> CancelOrder {
18099        CancelOrder::new(
18100            TraderId::from("TRADER-001"),
18101            Some(ClientId::from("BLOCKCHAIN-001")),
18102            StrategyId::from("S-001"),
18103            instrument_id,
18104            client_order_id,
18105            None,
18106            UUID4::new(),
18107            UnixNanos::default(),
18108            None,
18109            None,
18110        )
18111    }
18112
18113    fn cancel_all_orders_cmd(instrument_id: InstrumentId) -> CancelAllOrders {
18114        CancelAllOrders::new(
18115            TraderId::from("TRADER-001"),
18116            Some(ClientId::from("BLOCKCHAIN-001")),
18117            StrategyId::from("S-001"),
18118            instrument_id,
18119            Some(OrderSide::Sell),
18120            UUID4::new(),
18121            UnixNanos::default(),
18122            None,
18123            None,
18124        )
18125    }
18126
18127    fn batch_cancel_orders_cmd(cancels: Vec<CancelOrder>) -> BatchCancelOrders {
18128        BatchCancelOrders::new(
18129            TraderId::from("TRADER-001"),
18130            Some(ClientId::from("BLOCKCHAIN-001")),
18131            StrategyId::from("S-001"),
18132            cancels[0].instrument_id,
18133            cancels,
18134            UUID4::new(),
18135            UnixNanos::default(),
18136            None,
18137            None,
18138        )
18139    }
18140
18141    fn query_order_cmd(instrument_id: InstrumentId, client_order_id: ClientOrderId) -> QueryOrder {
18142        QueryOrder::new(
18143            TraderId::from("TRADER-001"),
18144            Some(ClientId::from("BLOCKCHAIN-001")),
18145            StrategyId::from("S-001"),
18146            instrument_id,
18147            client_order_id,
18148            None,
18149            UUID4::new(),
18150            UnixNanos::default(),
18151            None,
18152            None,
18153        )
18154    }
18155
18156    async fn unsupported_client_with_mock_rpc()
18157    -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
18158        let state = ready_rpc_state();
18159        let addr = start_mock_rpc_server(state.clone()).await;
18160        let (client, cache) = swap_client_with_cache(test_config(format!("http://{addr}")));
18161        (client, state, cache)
18162    }
18163
18164    #[tokio::test]
18165    async fn submit_order_list_denies_every_order_without_side_effects() {
18166        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18167        let pool = test_pool();
18168        let first = test_market_sell_order(pool.instrument_id);
18169        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18170        cache
18171            .borrow_mut()
18172            .add_order(second.clone(), None, None, false)
18173            .unwrap();
18174        let orders = [first.clone(), second.clone()];
18175        let mut receiver = start_with_events(&mut client);
18176
18177        client
18178            .submit_order_list(submit_order_list_cmd(&orders))
18179            .unwrap();
18180
18181        let events = collect_order_events(&mut receiver);
18182        let mut denied_ids = Vec::new();
18183
18184        for event in &events {
18185            let OrderEventAny::Denied(denied) = event else {
18186                panic!("expected OrderDenied, was {event:?}");
18187            };
18188            assert_eq!(denied.reason.as_str(), ORDER_LIST_UNSUPPORTED);
18189            denied_ids.push(denied.client_order_id);
18190        }
18191        denied_ids.sort();
18192        assert_eq!(
18193            denied_ids,
18194            [first.client_order_id(), second.client_order_id()]
18195        );
18196        assert!(state.recorded_requests().is_empty());
18197        assert!(client.in_flight.lock().is_none());
18198
18199        for order in &orders {
18200            let cache_ref = cache.borrow();
18201            let cached = cache_ref.order(&order.client_order_id()).unwrap();
18202            assert_eq!(cached.status(), OrderStatus::Initialized);
18203        }
18204    }
18205
18206    #[tokio::test]
18207    async fn modify_order_rejects_without_side_effects() {
18208        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18209        let order = test_market_sell_order(test_pool().instrument_id);
18210        let mut receiver = start_with_events(&mut client);
18211
18212        client
18213            .modify_order(modify_order_cmd(
18214                order.instrument_id(),
18215                order.client_order_id(),
18216            ))
18217            .unwrap();
18218
18219        let events = collect_order_events(&mut receiver);
18220        assert_eq!(events.len(), 1, "was: {events:?}");
18221        let OrderEventAny::ModifyRejected(rejected) = &events[0] else {
18222            panic!("expected OrderModifyRejected, was {:?}", events[0]);
18223        };
18224        assert_eq!(rejected.client_order_id, order.client_order_id());
18225        assert_eq!(rejected.reason.as_str(), ORDER_MODIFY_UNSUPPORTED);
18226        assert!(state.recorded_requests().is_empty());
18227        assert!(client.in_flight.lock().is_none());
18228        let cache_ref = cache.borrow();
18229        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18230        assert_eq!(cached.status(), OrderStatus::Initialized);
18231    }
18232
18233    #[tokio::test]
18234    async fn cancel_order_rejects_without_side_effects() {
18235        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18236        let order = test_market_sell_order(test_pool().instrument_id);
18237        let mut receiver = start_with_events(&mut client);
18238
18239        client
18240            .cancel_order(cancel_order_cmd(
18241                order.instrument_id(),
18242                order.client_order_id(),
18243            ))
18244            .unwrap();
18245
18246        let events = collect_order_events(&mut receiver);
18247        assert_eq!(events.len(), 1, "was: {events:?}");
18248        let OrderEventAny::CancelRejected(rejected) = &events[0] else {
18249            panic!("expected OrderCancelRejected, was {:?}", events[0]);
18250        };
18251        assert_eq!(rejected.client_order_id, order.client_order_id());
18252        assert_eq!(rejected.reason.as_str(), ORDER_CANCEL_UNSUPPORTED);
18253        assert!(state.recorded_requests().is_empty());
18254        assert!(client.in_flight.lock().is_none());
18255        let cache_ref = cache.borrow();
18256        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18257        assert_eq!(cached.status(), OrderStatus::Initialized);
18258    }
18259
18260    #[tokio::test]
18261    async fn batch_cancel_orders_rejects_each_order_without_side_effects() {
18262        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18263        let pool = test_pool();
18264        let first = test_market_sell_order(pool.instrument_id);
18265        let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18266        cache
18267            .borrow_mut()
18268            .add_order(second.clone(), None, None, false)
18269            .unwrap();
18270        let orders = [first.clone(), second.clone()];
18271        let cancels = orders
18272            .iter()
18273            .map(|order| cancel_order_cmd(order.instrument_id(), order.client_order_id()))
18274            .collect();
18275        let mut receiver = start_with_events(&mut client);
18276
18277        client
18278            .batch_cancel_orders(batch_cancel_orders_cmd(cancels))
18279            .unwrap();
18280
18281        let events = collect_order_events(&mut receiver);
18282        let mut rejected_ids = Vec::new();
18283
18284        for event in &events {
18285            let OrderEventAny::CancelRejected(rejected) = event else {
18286                panic!("expected OrderCancelRejected, was {event:?}");
18287            };
18288            assert_eq!(rejected.reason.as_str(), ORDER_CANCEL_UNSUPPORTED);
18289            rejected_ids.push(rejected.client_order_id);
18290        }
18291        rejected_ids.sort();
18292        assert_eq!(
18293            rejected_ids,
18294            [first.client_order_id(), second.client_order_id()]
18295        );
18296        assert!(state.recorded_requests().is_empty());
18297        assert!(client.in_flight.lock().is_none());
18298    }
18299
18300    #[tokio::test]
18301    async fn cancel_all_and_query_order_log_unsupported_without_side_effects() {
18302        let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18303        let order = test_market_sell_order(test_pool().instrument_id);
18304        let mut receiver = start_with_events(&mut client);
18305
18306        client
18307            .cancel_all_orders(cancel_all_orders_cmd(order.instrument_id()))
18308            .unwrap();
18309        client
18310            .query_order(query_order_cmd(
18311                order.instrument_id(),
18312                order.client_order_id(),
18313            ))
18314            .unwrap();
18315
18316        let events = collect_order_events(&mut receiver);
18317        assert!(events.is_empty(), "was: {events:?}");
18318        assert!(state.recorded_requests().is_empty());
18319        assert!(client.in_flight.lock().is_none());
18320        let cache_ref = cache.borrow();
18321        let cached = cache_ref.order(&order.client_order_id()).unwrap();
18322        assert_eq!(cached.status(), OrderStatus::Initialized);
18323    }
18324
18325    #[tokio::test]
18326    async fn unsupported_commands_handle_unknown_orders_without_panic() {
18327        let (mut client, state, _) = unsupported_client_with_mock_rpc().await;
18328        let instrument_id = test_pool().instrument_id;
18329        let unknown = ClientOrderId::from("O-UNKNOWN");
18330        let mut receiver = start_with_events(&mut client);
18331
18332        client
18333            .modify_order(modify_order_cmd(instrument_id, unknown))
18334            .unwrap();
18335        client
18336            .cancel_order(cancel_order_cmd(instrument_id, unknown))
18337            .unwrap();
18338        client
18339            .batch_cancel_orders(batch_cancel_orders_cmd(vec![cancel_order_cmd(
18340                instrument_id,
18341                unknown,
18342            )]))
18343            .unwrap();
18344        client
18345            .query_order(query_order_cmd(instrument_id, unknown))
18346            .unwrap();
18347
18348        let events = collect_order_events(&mut receiver);
18349        assert!(events.is_empty(), "was: {events:?}");
18350        assert!(state.recorded_requests().is_empty());
18351        assert!(client.in_flight.lock().is_none());
18352    }
18353
18354    #[tokio::test]
18355    async fn report_generators_error_except_mass_status_without_side_effects() {
18356        let (client, state, _) = unsupported_client_with_mock_rpc().await;
18357
18358        let report = client
18359            .generate_order_status_report(&GenerateOrderStatusReport::new(
18360                UUID4::new(),
18361                UnixNanos::default(),
18362                None,
18363                None,
18364                None,
18365                None,
18366                None,
18367            ))
18368            .await
18369            .unwrap_err();
18370        assert_eq!(report.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18371
18372        let reports = client
18373            .generate_order_status_reports(&GenerateOrderStatusReports::new(
18374                UUID4::new(),
18375                UnixNanos::default(),
18376                false,
18377                None,
18378                None,
18379                None,
18380                None,
18381                None,
18382            ))
18383            .await
18384            .unwrap_err();
18385        assert_eq!(reports.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18386
18387        let fills = client
18388            .generate_fill_reports(GenerateFillReports::new(
18389                UUID4::new(),
18390                UnixNanos::default(),
18391                None,
18392                None,
18393                None,
18394                None,
18395                None,
18396                None,
18397            ))
18398            .await
18399            .unwrap_err();
18400        assert_eq!(fills.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18401
18402        let positions = client
18403            .generate_position_status_reports(&GeneratePositionStatusReports::new(
18404                UUID4::new(),
18405                UnixNanos::default(),
18406                None,
18407                None,
18408                None,
18409                None,
18410                None,
18411            ))
18412            .await
18413            .unwrap_err();
18414        assert_eq!(positions.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18415
18416        let mass_status = client.generate_mass_status(None).await.unwrap();
18417        assert!(mass_status.is_none());
18418
18419        let mass_status = client.generate_mass_status(Some(60)).await.unwrap();
18420        assert!(mass_status.is_none());
18421
18422        assert!(state.recorded_requests().is_empty());
18423        assert!(client.in_flight.lock().is_none());
18424    }
18425
18426    async fn connect_test_postgres(
18427        test_name: &str,
18428    ) -> Option<(sqlx::PgPool, PostgresConnectOptions)> {
18429        let pg_config = get_postgres_connect_options(None, None, None, None, None);
18430        let admin_options: sqlx::postgres::PgConnectOptions = pg_config.clone().into();
18431        let admin_pool = match PgPoolOptions::new()
18432            .max_connections(1)
18433            .connect_with(admin_options)
18434            .await
18435        {
18436            Ok(pool) => pool,
18437            Err(e) => {
18438                eprintln!("Postgres unavailable; skipping {test_name} test: {e}");
18439                return None;
18440            }
18441        };
18442
18443        Some((admin_pool, pg_config))
18444    }
18445
18446    async fn execution_client_with_database(
18447        test_name: &str,
18448        state: MockRpcState,
18449    ) -> Option<(
18450        sqlx::PgPool,
18451        String,
18452        BlockchainExecutionClient,
18453        MockRpcState,
18454    )> {
18455        let (admin_pool, schema, mut client, state) =
18456            execution_client_with_unprotected_database(test_name, state).await?;
18457        protect_test_storage(&mut client, &schema).await;
18458        Some((admin_pool, schema, client, state))
18459    }
18460
18461    async fn execution_client_with_unprotected_database(
18462        test_name: &str,
18463        state: MockRpcState,
18464    ) -> Option<(
18465        sqlx::PgPool,
18466        String,
18467        BlockchainExecutionClient,
18468        MockRpcState,
18469    )> {
18470        let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
18471        let schema = format!("{test_name}_{}", std::process::id());
18472        setup_execution_schema(&admin_pool, &schema).await;
18473
18474        let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
18475        let db_options = db_options.options([("search_path", schema.clone())]);
18476        let database = connect_test_database(db_options).await.unwrap();
18477        let addr = start_mock_rpc_server(state.clone()).await;
18478        let mut client = test_client(format!("http://{addr}"));
18479        client.cache.database = Some(database);
18480        // Mirror the connect-time migration: tests create the pre-submission table shape
18481        client
18482            .cache
18483            .ensure_execution_transaction_schema()
18484            .await
18485            .unwrap();
18486        initialize_test_verification_ledger(&client).await;
18487        client.signer = Some(Arc::new(
18488            PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
18489        ));
18490        client.core.set_connected();
18491
18492        Some((admin_pool, schema, client, state))
18493    }
18494
18495    async fn install_reservation_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18496        for statement in [
18497            format!(
18498                "CREATE FUNCTION {schema}.reject_execution_reservation_commit() RETURNS trigger \
18499                 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test reservation commit rejection''; \
18500                 RETURN NEW; END'"
18501            ),
18502            format!(
18503                "CREATE CONSTRAINT TRIGGER reject_execution_reservation_commit AFTER INSERT ON \
18504                 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18505                 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_execution_reservation_commit()"
18506            ),
18507        ] {
18508            sqlx::query(sqlx::AssertSqlSafe(statement))
18509                .execute(admin_pool)
18510                .await
18511                .unwrap();
18512        }
18513    }
18514
18515    async fn install_recoverable_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18516        for statement in [
18517            format!(
18518                "CREATE FUNCTION {schema}.reject_recoverable_commit() RETURNS trigger \
18519                 LANGUAGE plpgsql AS 'BEGIN IF NEW.transition_key = ''recoverable'' THEN \
18520                 RAISE EXCEPTION ''test recoverable commit rejection''; END IF; \
18521                 RETURN NEW; END'"
18522            ),
18523            format!(
18524                "CREATE CONSTRAINT TRIGGER reject_recoverable_commit AFTER INSERT ON \
18525                 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18526                 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_recoverable_commit()"
18527            ),
18528        ] {
18529            sqlx::query(sqlx::AssertSqlSafe(statement))
18530                .execute(admin_pool)
18531                .await
18532                .unwrap();
18533        }
18534    }
18535
18536    async fn drop_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18537        sqlx::query(sqlx::AssertSqlSafe(format!("DROP SCHEMA {schema} CASCADE")))
18538            .execute(admin_pool)
18539            .await
18540            .unwrap();
18541    }
18542
18543    async fn setup_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18544        for statement in [
18545            format!("CREATE SCHEMA {schema}"),
18546            format!(
18547                r#"CREATE TABLE {schema}."chain" (chain_id INTEGER PRIMARY KEY, name TEXT NOT NULL)"#
18548            ),
18549            format!(r#"INSERT INTO {schema}."chain" (chain_id, name) VALUES (42161, 'Arbitrum')"#),
18550            format!(
18551                r#"CREATE TABLE {schema}."execution_transaction" (
18552                    id BIGSERIAL PRIMARY KEY,
18553                    chain_id INTEGER NOT NULL REFERENCES {schema}."chain"(chain_id) ON DELETE CASCADE,
18554                    nonce BIGINT NOT NULL,
18555                    transaction_hash TEXT NOT NULL,
18556                    purpose TEXT NOT NULL,
18557                    status TEXT NOT NULL,
18558                    UNIQUE (chain_id, transaction_hash)
18559                )"#
18560            ),
18561        ] {
18562            sqlx::query(sqlx::AssertSqlSafe(statement))
18563                .execute(admin_pool)
18564                .await
18565                .unwrap();
18566        }
18567    }
18568
18569    fn execution_transaction_create_sql() -> &'static str {
18570        const TABLES_SQL: &str = include_str!("../../../../../schema/sql/tables.sql");
18571        const START: &str = "CREATE TABLE IF NOT EXISTS \"execution_transaction\"";
18572        let start = TABLES_SQL
18573            .find(START)
18574            .expect("execution_transaction table is missing from tables.sql");
18575        let statement = &TABLES_SQL[start..];
18576        let end = statement
18577            .find(";\n")
18578            .expect("execution_transaction CREATE TABLE is unterminated")
18579            + 1;
18580        &statement[..end]
18581    }
18582}