1use std::{
17 collections::{HashMap, HashSet},
18 fmt::Debug,
19 ops::RangeInclusive,
20 str::FromStr,
21 sync::Arc,
22 time::{Duration, SystemTime, UNIX_EPOCH},
23};
24
25use alloy::{
26 primitives::{
27 Address, B256, Bytes, I256, U256,
28 aliases::{U24, U160},
29 keccak256,
30 },
31 signers::local::PrivateKeySigner,
32 sol_types::SolCall,
33};
34use anyhow::Context;
35use async_trait::async_trait;
36use nautilus_common::{
37 clients::ExecutionClient,
38 live::runner::get_exec_event_sender,
39 messages::execution::{
40 BatchCancelOrders, CancelAllOrders, CancelOrder, GenerateFillReports,
41 GenerateOrderStatusReport, GenerateOrderStatusReports, GeneratePositionStatusReports,
42 ModifyOrder, QueryAccount, QueryOrder, SubmitOrder, SubmitOrderList,
43 },
44};
45use nautilus_core::{
46 Params, UUID4, UnixNanos, datetime::NANOSECONDS_IN_SECOND, hex, time::get_atomic_clock_realtime,
47};
48use nautilus_live::{
49 ExecutionClientCore, ExecutionEventEmitter,
50 task::{TaskGroup, TaskGroupGuard},
51};
52use nautilus_model::{
53 accounts::AccountAny,
54 defi::{
55 DexType, Pool, PoolIdentifier, SharedChain, Token,
56 data::block::{BLOCK_SCOPED_SNAPSHOT_INDEX, BlockPosition},
57 pool_analysis::quote::SwapQuote,
58 validation::validate_address,
59 wallet::{TokenBalance, WalletBalance},
60 },
61 enums::{CurrencyType, LiquiditySide, OmsType, OrderSide, OrderStatus, OrderType},
62 events::{OrderCanceled, OrderDeniedReason, OrderEventAny, OrderFilled, OrderRejected},
63 identifiers::{AccountId, ClientId, ClientOrderId, InstrumentId, TradeId, Venue, VenueOrderId},
64 orders::{Order, OrderAny},
65 reports::{ExecutionMassStatus, FillReport, OrderStatusReport, PositionStatusReport},
66 types::{
67 AccountBalance, Currency, MarginBalance, Money, Price, Quantity, fixed::FIXED_PRECISION,
68 },
69};
70use parking_lot::Mutex;
71use zeroize::Zeroizing;
72
73use crate::{
74 cache::{
75 BlockchainCache,
76 database::{
77 BlockchainCacheDatabase, ExecutionFinalityTransition, ExecutionNonceAssignment,
78 ExecutionPayloadCheck, ExecutionPayloadLease, ExecutionReplacementScan,
79 ExecutionVerificationBatch, ExecutionVerificationBootstrap,
80 ExecutionVerificationDecision, ExecutionVerificationMigration,
81 ExecutionVerificationMigrationRecord, ExecutionVerificationMigrationSnapshot,
82 ExecutionVerifiedHeader, reservation_failure_proven_not_committed,
83 },
84 rows::{ExecutionIntentInsert, ExecutionIntentRow, ExecutionTransactionHashRow},
85 },
86 config::{
87 BlockchainContractRole, BlockchainDeploymentManifest, BlockchainExecutionClientConfig,
88 },
89 contracts::{
90 erc20::{ERC20, Erc20Contract},
91 uniswap_v3_quote::UniswapV3Quote,
92 uniswap_v3_swap::{UniswapV3Factory, UniswapV3RouterState, UniswapV3SwapRouter},
93 weth::WETH9,
94 },
95 execution::{
96 preflight::{
97 BlockchainPreflightReport, ContractCodeCheck, PoolPreflightCheck, TokenPreflightCheck,
98 },
99 sealing::{
100 PayloadKeySet, PayloadPolicy, authenticate_payload, authenticate_payload_identity,
101 authenticate_retained_payload, payload_context, payload_context_identity,
102 persisted_call_fields, retained_payload_requires_policy,
103 },
104 transaction::{
105 TransactionPurpose, TransactionStatus, build_eip1559_transaction, compute_max_fee,
106 decode_signed_transaction, derive_fees, derive_gas_limit, sign_eip1559_transaction,
107 },
108 },
109 rpc::{
110 error::BroadcastError,
111 http::{BlockchainHttpRpcClient, EXECUTION_RPC_TIMEOUT_SECS},
112 log as rpc_log,
113 types::{RpcCallType, RpcTransaction, RpcTransactionReceipt},
114 verification::{
115 VerificationCoordinator, VerificationOutcome, Verified, VerifiedBlockHeader,
116 VerifiedCallTrace, VerifiedSimulation,
117 },
118 },
119};
120
121const RECEIPT_POLL_INTERVAL: Duration = Duration::from_secs(1);
122const MAX_PAYLOAD_OPERATION_BATCH_SIZE: usize = 1_000;
123const BPS_DENOMINATOR: u32 = 10_000;
124const ORDER_LIST_UNSUPPORTED: &str =
126 "Order lists are not supported; submit each order individually";
127const ORDER_MODIFY_UNSUPPORTED: &str = "Order modification is not supported";
129const ORDER_CANCEL_UNSUPPORTED: &str = "Order cancellation is not supported";
131
132const VENUE_EXECUTION_REPORTS_UNSUPPORTED: &str =
134 "Venue execution reports are not supported on the blockchain execution client";
135const MAX_REPLACEMENT_SCAN_BLOCKS: u64 = 4_096;
136
137#[derive(Debug)]
139pub struct BlockchainExecutionClient {
140 core: ExecutionClientCore,
141 emitter: ExecutionEventEmitter,
142 cache: BlockchainCache,
143 config: BlockchainExecutionClientConfig,
144 chain: SharedChain,
145 wallet_address: Address,
146 signer: Option<Arc<PrivateKeySigner>>,
147 payload_keys: Option<Arc<PayloadKeySet>>,
148 router_addresses: Vec<Address>,
149 transaction_limits: TransactionLimits,
150 weth_address: Address,
151 in_flight: Arc<Mutex<Option<InFlightSlot>>>,
152 wallet_balance: Arc<Mutex<WalletBalance>>,
153 erc20_contract: Erc20Contract,
154 http_rpc_client: Arc<BlockchainHttpRpcClient>,
155 verification: VerificationCoordinator,
156 pending_tasks: TaskGroup,
157}
158
159impl BlockchainExecutionClient {
160 pub fn new(
169 core_client: ExecutionClientCore,
170 config: BlockchainExecutionClientConfig,
171 ) -> anyhow::Result<Self> {
172 let transaction_limits = Self::transaction_limits(&config)?;
173 let chain = Arc::new(config.chain.clone());
174 let cache = BlockchainCache::new(Arc::clone(&chain));
175 let http_rpc_client = Arc::new(BlockchainHttpRpcClient::new(
176 config.http_rpc_url.clone().into_inner(),
177 config.rpc_requests_per_second,
178 None,
179 ));
180 let verification_config = config.verification.as_ref().ok_or_else(|| {
181 anyhow::anyhow!("Independent Blockchain execution verification is required")
182 })?;
183 anyhow::ensure!(
184 verification_config.chain_anchor.chain_id == config.chain.chain_id,
185 "Verification chain anchor ID does not match the configured chain"
186 );
187 anyhow::ensure!(
188 verification_config.chain_anchor.chain_name == config.chain.name.to_string(),
189 "Verification chain anchor name does not match the configured chain"
190 );
191 let verification = VerificationCoordinator::new(
192 Arc::clone(&http_rpc_client),
193 config.http_rpc_url.expose_secret(),
194 verification_config,
195 config.rpc_requests_per_second,
196 )?;
197 let wallet_address = validate_address(config.wallet_address.as_str())?;
198 let erc20_contract = Erc20Contract::new_with_timeout(
199 Arc::clone(&http_rpc_client),
200 Some(EXECUTION_RPC_TIMEOUT_SECS),
201 true,
202 );
203
204 let router_addresses = config
205 .router_addresses
206 .iter()
207 .map(|address| validate_address(address.as_str()))
208 .collect::<anyhow::Result<Vec<_>>>()?;
209 if router_addresses.is_empty() {
210 anyhow::bail!("`router_addresses` must contain at least one router address");
211 }
212 let weth_address = validate_address(config.weth_address.as_str())?;
213 Self::validate_manifest_contracts(&config, &router_addresses, weth_address)?;
214
215 let mut token_universe = HashSet::new();
216
217 if let Some(specified_tokens) = &config.tokens {
218 for token in specified_tokens {
219 let token_address = validate_address(token.as_str())?;
220 token_universe.insert(token_address);
221 }
222 }
223 let wallet_balance = WalletBalance::new(token_universe);
224 let emitter = ExecutionEventEmitter::new(
225 get_atomic_clock_realtime(),
226 core_client.trader_id,
227 core_client.account_id,
228 core_client.account_type,
229 core_client.base_currency,
230 );
231
232 let pending_tasks = TaskGroup::new();
233
234 Ok(Self {
235 core: core_client,
236 emitter,
237 wallet_balance: Arc::new(Mutex::new(wallet_balance)),
238 chain,
239 cache,
240 config,
241 signer: None,
242 payload_keys: None,
243 router_addresses,
244 transaction_limits,
245 weth_address,
246 in_flight: Arc::new(Mutex::new(None)),
247 erc20_contract,
248 http_rpc_client,
249 verification,
250 wallet_address,
251 pending_tasks,
252 })
253 }
254
255 fn transaction_limits(
256 config: &BlockchainExecutionClientConfig,
257 ) -> anyhow::Result<TransactionLimits> {
258 let (
259 Some(allowed_token_pairs),
260 Some(slippage_bps),
261 Some(max_slippage_bps),
262 Some(max_order_amount),
263 Some(deadline_seconds),
264 Some(max_quote_age_blocks),
265 Some(receipt_timeout_secs),
266 ) = (
267 &config.allowed_token_pairs,
268 config.slippage_bps,
269 config.max_slippage_bps,
270 config.max_order_amount,
271 config.deadline_seconds,
272 config.max_quote_age_blocks,
273 config.receipt_timeout_secs,
274 )
275 else {
276 anyhow::bail!(
277 "Blockchain execution transaction limits are required: allowed_token_pairs, slippage_bps, max_slippage_bps, max_order_amount, deadline_seconds, max_quote_age_blocks, receipt_timeout_secs"
278 );
279 };
280
281 let mut parsed_pairs = HashSet::with_capacity(allowed_token_pairs.len());
282 for (token_in, token_out) in allowed_token_pairs {
283 parsed_pairs.insert((
284 validate_address(token_in.as_str())?,
285 validate_address(token_out.as_str())?,
286 ));
287 }
288
289 let quote_spend_limits = config.quote_spend_limits.as_deref().unwrap_or_default();
290 let mut parsed_quote_spend_limits = HashMap::with_capacity(quote_spend_limits.len());
291 for limit in quote_spend_limits {
292 let token_in = validate_address(limit.token_in.as_str())?;
293 let token_out = validate_address(limit.token_out.as_str())?;
294 let spend_token = validate_address(limit.spend_token.as_str())?;
295
296 if !parsed_pairs.contains(&(token_in, token_out)) {
297 anyhow::bail!(
298 "Quote spend limit pair {token_in} -> {token_out} is not in the `allowed_token_pairs` allowlist"
299 );
300 }
301
302 if spend_token != token_in {
303 anyhow::bail!(
304 "Quote spend limit for {token_in} -> {token_out} is denominated in {spend_token}; `spend_token` must match `token_in`"
305 );
306 }
307
308 if limit.max_amount.is_empty()
309 || !limit.max_amount.bytes().all(|byte| byte.is_ascii_digit())
310 {
311 anyhow::bail!(
312 "Quote spend limit `max_amount` '{}' must be a base-10 unsigned integer string",
313 limit.max_amount
314 );
315 }
316 let max_amount = U256::from_str(&limit.max_amount).map_err(|_| {
317 anyhow::anyhow!(
318 "Quote spend limit `max_amount` '{}' exceeds the U256 range",
319 limit.max_amount
320 )
321 })?;
322 let ceiling = QuoteSpendCeiling {
323 spend_token,
324 spend_token_decimals: limit.spend_token_decimals,
325 max_amount,
326 };
327
328 if parsed_quote_spend_limits
329 .insert((token_in, token_out), ceiling)
330 .is_some()
331 {
332 anyhow::bail!(
333 "Duplicate quote spend limit for token pair {token_in} -> {token_out}"
334 );
335 }
336 }
337
338 if slippage_bps > max_slippage_bps {
339 anyhow::bail!(
340 "`slippage_bps` {slippage_bps} exceeds `max_slippage_bps` {max_slippage_bps}"
341 );
342 }
343
344 if max_slippage_bps >= BPS_DENOMINATOR {
345 anyhow::bail!("`max_slippage_bps` {max_slippage_bps} must be below {BPS_DENOMINATOR}");
346 }
347
348 if !(1..=4_095).contains(&max_quote_age_blocks) {
349 anyhow::bail!("`max_quote_age_blocks` must be in 1..=4095");
350 }
351
352 Ok(TransactionLimits {
353 allowed_token_pairs: parsed_pairs,
354 quote_spend_limits: parsed_quote_spend_limits,
355 slippage_bps,
356 max_slippage_bps,
357 max_order_amount,
358 deadline_seconds,
359 max_quote_age_blocks,
360 receipt_timeout_secs,
361 })
362 }
363
364 fn validate_manifest_contracts(
365 config: &BlockchainExecutionClientConfig,
366 routers: &[Address],
367 weth: Address,
368 ) -> anyhow::Result<()> {
369 let verification = config.verification.as_ref().ok_or_else(|| {
370 anyhow::anyhow!("Independent Blockchain execution verification is required")
371 })?;
372 let manifest = &verification.deployment_manifest;
373 let role_addresses = |role| {
374 manifest
375 .contracts
376 .iter()
377 .filter(|contract| contract.role == role)
378 .map(|contract| {
379 Address::from_str(&contract.address)
380 .map_err(|_| anyhow::anyhow!("Deployment manifest address is invalid"))
381 })
382 .collect::<anyhow::Result<HashSet<_>>>()
383 };
384 let singleton = |role, description: &str| {
385 let addresses = role_addresses(role)?;
386 anyhow::ensure!(
387 addresses.len() == 1,
388 "Deployment manifest must contain exactly one {description} contract"
389 );
390 Ok(*addresses.iter().next().expect("singleton role address"))
391 };
392
393 let configured_routers = routers.iter().copied().collect::<HashSet<_>>();
394 anyhow::ensure!(
395 role_addresses(BlockchainContractRole::Router)? == configured_routers,
396 "Deployment manifest router set does not match `router_addresses`"
397 );
398 anyhow::ensure!(
399 singleton(BlockchainContractRole::WrappedNative, "wrapped native")? == weth,
400 "Deployment manifest wrapped native contract does not match `weth_address`"
401 );
402 let factory = singleton(BlockchainContractRole::Factory, "factory")?;
403 let registered_factory =
404 crate::exchanges::get_dex_extended(config.chain.name, &DexType::UniswapV3)
405 .map(|dex| dex.factory)
406 .ok_or_else(|| {
407 anyhow::anyhow!(
408 "No registered Uniswap V3 deployment for chain {}",
409 config.chain.name
410 )
411 })?;
412 anyhow::ensure!(
413 factory == registered_factory,
414 "Deployment manifest factory does not match the registered Uniswap V3 factory"
415 );
416 let quote_contract = singleton(BlockchainContractRole::Quote, "quote")?;
417
418 let mut token_decimals = HashMap::new();
419
420 for token in &manifest.tokens {
421 let address = Address::from_str(&token.address)
422 .map_err(|_| anyhow::anyhow!("Deployment manifest token address is invalid"))?;
423 anyhow::ensure!(
424 token_decimals.insert(address, token.decimals).is_none(),
425 "Deployment manifest contains a duplicate token identity"
426 );
427 }
428 anyhow::ensure!(
429 token_decimals.contains_key(&weth),
430 "Deployment manifest has no wrapped native token identity"
431 );
432
433 if let Some(tokens) = &config.tokens {
434 for token in tokens {
435 let address = validate_address(token)?;
436 anyhow::ensure!(
437 token_decimals.contains_key(&address),
438 "Configured token {address} has no deployment manifest identity"
439 );
440 }
441 }
442
443 for (token_in, token_out) in config.allowed_token_pairs.as_deref().unwrap_or_default() {
444 let token_in = validate_address(token_in)?;
445 let token_out = validate_address(token_out)?;
446 anyhow::ensure!(
447 token_in != token_out
448 && token_decimals.contains_key(&token_in)
449 && token_decimals.contains_key(&token_out),
450 "Allowed token pair {token_in} -> {token_out} is not fully pinned by the deployment manifest"
451 );
452 }
453
454 for limit in config.quote_spend_limits.as_deref().unwrap_or_default() {
455 let spend_token = validate_address(&limit.spend_token)?;
456 anyhow::ensure!(
457 token_decimals.get(&spend_token) == Some(&limit.spend_token_decimals),
458 "Quote spend limit decimals do not match the deployment manifest"
459 );
460 }
461
462 let pool_contracts = role_addresses(BlockchainContractRole::Pool)?;
463
464 for pool in &manifest.pools {
465 let pool_address = Address::from_str(&pool.address)
466 .map_err(|_| anyhow::anyhow!("Deployment manifest pool address is invalid"))?;
467 let pool_factory = Address::from_str(&pool.factory)
468 .map_err(|_| anyhow::anyhow!("Deployment manifest pool factory is invalid"))?;
469 let pool_quote = Address::from_str(&pool.quote_contract).map_err(|_| {
470 anyhow::anyhow!("Deployment manifest pool quote contract is invalid")
471 })?;
472 anyhow::ensure!(
473 pool_contracts.contains(&pool_address)
474 && pool_factory == factory
475 && pool_quote == quote_contract,
476 "Deployment manifest pool does not use the pinned pool, factory, and quote identities"
477 );
478 }
479 Ok(())
480 }
481
482 async fn fetch_native_currency_balance(&self) -> anyhow::Result<Money> {
483 let balance_u256 = self
484 .http_rpc_client
485 .get_balance_with_timeout(&self.wallet_address, None, Some(EXECUTION_RPC_TIMEOUT_SECS))
486 .await?;
487
488 let native_currency = self.chain.native_currency();
489
490 Money::from_u256(balance_u256, native_currency).map_err(Into::into)
491 }
492
493 async fn fetch_token_balance(
494 &mut self,
495 token_address: &Address,
496 ) -> anyhow::Result<TokenBalance> {
497 let token = if let Some(token) = self.cache.get_token(token_address) {
498 token.to_owned()
499 } else {
500 let token_info = self.erc20_contract.fetch_token_info(token_address).await?;
501 let token = Token::new(
502 Arc::clone(&self.chain),
503 *token_address,
504 token_info.name,
505 token_info.symbol,
506 token_info.decimals,
507 );
508 self.cache.add_token(token.clone()).await?;
509 token
510 };
511
512 let amount = self
513 .erc20_contract
514 .balance_of(token_address, &self.wallet_address)
515 .await?;
516 let token_balance = TokenBalance::new(amount, token);
517
518 Ok(token_balance)
522 }
523
524 async fn refresh_wallet_balances(&mut self) -> anyhow::Result<()> {
526 let (wallet_balance, balances) = self.fetch_wallet_balances().await?;
527 self.generate_account_state(
528 balances,
529 vec![],
530 true,
531 get_atomic_clock_realtime().get_time_ns(),
532 None,
533 )?;
534 *self.wallet_balance.lock() = wallet_balance;
535 Ok(())
536 }
537
538 async fn fetch_wallet_balances(
539 &mut self,
540 ) -> anyhow::Result<(WalletBalance, Vec<AccountBalance>)> {
541 let native_currency_balance = self.fetch_native_currency_balance().await?;
542 let token_universe = self.wallet_balance.lock().token_universe.clone();
543 let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
544 token_addresses.sort_unstable();
545
546 let mut token_balances = Vec::with_capacity(token_addresses.len());
547 for token_address in token_addresses {
548 let token_balance = self
549 .fetch_token_balance(&token_address)
550 .await
551 .with_context(|| format!("failed to fetch token balance for {token_address}"))?;
552 token_balances.push(token_balance);
553 }
554
555 let mut wallet_balance = WalletBalance::new(token_universe);
556 let balances = wallet_balance.replace_balances(native_currency_balance, token_balances)?;
557 log::debug!(
558 "Refreshed wallet balance with {} account balances",
559 balances.len()
560 );
561 Ok((wallet_balance, balances))
562 }
563
564 pub async fn preflight(
576 &self,
577 instrument_id: &InstrumentId,
578 ) -> anyhow::Result<BlockchainPreflightReport> {
579 let pool = self.resolve_pool(instrument_id)?;
580 let base_token = pool.get_base_token().clone();
581 let quote_token = pool.get_quote_token().clone();
582
583 let actual_chain_id = self.http_rpc_client.chain_id().await?;
584
585 let pool_code = self.http_rpc_client.get_code(&pool.address).await?;
586 let pool_check = PoolPreflightCheck {
587 instrument_id: pool.instrument_id,
588 address: pool.address,
589 has_deployed_code: !pool_code.is_empty(),
590 fee: pool.fee,
591 base_token: base_token.address,
592 quote_token: quote_token.address,
593 };
594
595 let mut routers = Vec::with_capacity(self.router_addresses.len());
596 for router in &self.router_addresses {
597 let code = self.http_rpc_client.get_code(router).await?;
598 routers.push(ContractCodeCheck {
599 address: *router,
600 has_deployed_code: !code.is_empty(),
601 });
602 }
603
604 let mut tokens = Vec::with_capacity(2);
605
606 for token in [&base_token, "e_token] {
607 let code = self.http_rpc_client.get_code(&token.address).await?;
608 let wallet_balance = self
609 .erc20_contract
610 .balance_of(&token.address, &self.wallet_address)
611 .await?;
612
613 let mut router_allowances = Vec::new();
614 if token.address == base_token.address {
615 router_allowances.reserve(self.router_addresses.len());
616 for router in &self.router_addresses {
617 let amount = self
618 .erc20_contract
619 .allowance(&token.address, &self.wallet_address, router)
620 .await?;
621 router_allowances.push((*router, amount));
622 }
623 }
624
625 tokens.push(TokenPreflightCheck {
626 address: token.address,
627 symbol: token.symbol.clone(),
628 has_deployed_code: !code.is_empty(),
629 wallet_balance,
630 router_allowances,
631 });
632 }
633
634 let native_balance_wei = self
635 .http_rpc_client
636 .get_balance_with_timeout(&self.wallet_address, None, Some(EXECUTION_RPC_TIMEOUT_SECS))
637 .await?;
638
639 let latest_block = self.http_rpc_client.latest_block().await?;
640 let base_fee_per_gas_wei = latest_block.base_fee_per_gas.ok_or_else(|| {
641 anyhow::anyhow!("Latest block {} has no base fee", latest_block.number)
642 })?;
643 let max_priority_fee_per_gas_wei = self.http_rpc_client.max_priority_fee_per_gas().await?;
644 let derived_max_fee_per_gas_wei = compute_max_fee(
645 base_fee_per_gas_wei,
646 max_priority_fee_per_gas_wei,
647 self.config.base_fee_buffer_bps,
648 )?;
649
650 Ok(BlockchainPreflightReport::new(
651 u64::from(self.chain.chain_id),
652 actual_chain_id,
653 pool_check,
654 routers,
655 tokens,
656 native_balance_wei,
657 base_fee_per_gas_wei,
658 max_priority_fee_per_gas_wei,
659 derived_max_fee_per_gas_wei,
660 u128::from(self.config.max_fee_per_gas_wei),
661 ))
662 }
663
664 pub async fn wrap(&mut self, amount_wei: U256) -> anyhow::Result<B256> {
677 if amount_wei.is_zero() {
678 anyhow::bail!("Wrap amount must be positive");
679 }
680
681 self.ensure_transaction_ready(TransactionPurpose::Wrap)?;
682
683 let calldata = WETH9::depositCall {}.abi_encode();
684 let executor = self.transaction_executor()?;
685 let included = executor
686 .transact(
687 self.weth_address,
688 amount_wei,
689 Bytes::from(calldata),
690 TransactionPurpose::Wrap,
691 None,
692 TransactionAuthorization::Wrap {
693 weth: self.weth_address,
694 },
695 )
696 .await?;
697 let postconditions =
698 verify_wrap_balance_increase(&executor, &self.weth_address, amount_wei, &included)
699 .await?;
700 executor
701 .commit_verified_finality(&included, TransactionStatus::Finalized, &postconditions)
702 .await?;
703 executor
704 .database
705 .mark_execution_event_emitted(included.intent_id, "terminal")
706 .await?;
707 executor.release_slot();
708
709 Ok(included.tx_hash)
710 }
711
712 pub async fn approve(
727 &mut self,
728 token: Address,
729 amount: U256,
730 router: Address,
731 ) -> anyhow::Result<B256> {
732 if !self.router_addresses.contains(&router) {
733 anyhow::bail!("Router {router} is not in the configured `router_addresses` allowlist");
734 }
735
736 if !amount.is_zero()
737 && !self
738 .transaction_limits
739 .allowed_token_pairs
740 .iter()
741 .any(|(token_in, _)| *token_in == token)
742 {
743 anyhow::bail!(
744 "Token {token} is not an input token in the configured `allowed_token_pairs`"
745 );
746 }
747
748 self.ensure_transaction_ready(TransactionPurpose::Approve)?;
749
750 let approval_amount = if amount.is_zero() {
751 U256::ZERO
752 } else if self.config.unlimited_approval {
753 U256::MAX
754 } else {
755 amount
756 };
757 let calldata = ERC20::approveCall {
758 spender: router,
759 amount: approval_amount,
760 }
761 .abi_encode();
762
763 let executor = self.transaction_executor()?;
764 let included = executor
765 .transact(
766 token,
767 U256::ZERO,
768 Bytes::from(calldata),
769 TransactionPurpose::Approve,
770 None,
771 TransactionAuthorization::Approve {
772 token,
773 router,
774 amount: approval_amount,
775 },
776 )
777 .await?;
778 let postconditions =
779 verify_approve_allowance(&executor, &token, &router, approval_amount, &included)
780 .await?;
781 executor
782 .commit_verified_finality(&included, TransactionStatus::Finalized, &postconditions)
783 .await?;
784 executor
785 .database
786 .mark_execution_event_emitted(included.intent_id, "terminal")
787 .await?;
788 executor.release_slot();
789
790 Ok(included.tx_hash)
791 }
792
793 fn uniswap_v3_factory(&self) -> anyhow::Result<Address> {
794 crate::exchanges::get_dex_extended(self.chain.name, &DexType::UniswapV3)
795 .map(|dex| dex.factory)
796 .ok_or_else(|| {
797 anyhow::anyhow!(
798 "No registered Uniswap V3 deployment for chain {}",
799 self.chain.name
800 )
801 })
802 }
803
804 fn resolve_pool(&self, instrument_id: &InstrumentId) -> anyhow::Result<Pool> {
805 let (blockchain, dex_type) = instrument_id.venue.parse_dex()?;
806 if blockchain != self.chain.name {
807 anyhow::bail!(
808 "Pool venue chain {blockchain} does not match the client chain {}",
809 self.chain.name
810 );
811 }
812
813 if dex_type != DexType::UniswapV3 {
814 anyhow::bail!("Unsupported DEX type {dex_type}; only UniswapV3 is supported");
815 }
816
817 let pool_identifier = PoolIdentifier::new_checked(instrument_id.symbol.as_str())?;
818 if !pool_identifier.is_address() {
819 anyhow::bail!(
820 "Pool identifier {pool_identifier} is a pool ID; only address identifiers are supported"
821 );
822 }
823
824 let pool = self
825 .core
826 .cache()
827 .pool(instrument_id)
828 .cloned()
829 .ok_or_else(|| {
830 anyhow::anyhow!(
831 "Unknown pool {instrument_id}; not found in the shared engine cache"
832 )
833 })?;
834
835 if pool.token0.get_token_priority() == pool.token1.get_token_priority() {
836 anyhow::bail!(
837 "Pool {instrument_id} tokens share a token priority; base and quote orientation is ambiguous"
838 );
839 }
840
841 Ok(pool)
842 }
843
844 pub async fn check_payload_storage(
855 &self,
856 batch_size: usize,
857 ) -> anyhow::Result<PayloadStorageCheck> {
858 let batch_size = validate_payload_operation_batch_size(batch_size)?;
859 let database = self.payload_operation_database().await?;
860 let keys = self.load_payload_keys()?;
861 database
862 .check_execution_payload_storage(keys.as_ref(), None, batch_size)
863 .await
864 .map(Into::into)
865 }
866
867 pub async fn protect_payload_storage(&self) -> anyhow::Result<()> {
877 let database = self.payload_operation_database().await?;
878 database.ensure_execution_transaction_schema().await?;
879 let keys = self
880 .load_payload_keys()?
881 .ok_or_else(|| anyhow::anyhow!("Payload protection requires an active payload key"))?;
882 database.ensure_execution_payload_storage(&keys).await
883 }
884
885 pub async fn rewrap_payload_storage(&self, batch_size: usize) -> anyhow::Result<()> {
895 let batch_size = validate_payload_operation_batch_size(batch_size)?;
896 let database = self.payload_operation_database().await?;
897 let keys = self
898 .load_payload_keys()?
899 .ok_or_else(|| anyhow::anyhow!("Payload rewrap requires an active payload key"))?;
900 database
901 .rewrap_execution_payload_storage(&keys, batch_size)
902 .await
903 }
904
905 pub async fn rollback_payload_storage(&self, batch_size: usize) -> anyhow::Result<()> {
915 let batch_size = validate_payload_operation_batch_size(batch_size)?;
916 let database = self.payload_operation_database().await?;
917 let keys = self
918 .load_payload_keys()?
919 .ok_or_else(|| anyhow::anyhow!("Payload rollback requires an active payload key"))?;
920 database
921 .rollback_execution_payload_storage(&keys, batch_size)
922 .await
923 }
924
925 async fn payload_operation_database(&self) -> anyhow::Result<BlockchainCacheDatabase> {
926 anyhow::ensure!(
927 !self.core.is_connected(),
928 "Disconnect the execution client before payload storage operations"
929 );
930
931 if let Some(database) = &self.cache.database {
932 return Ok(database.clone());
933 }
934 let options = self
935 .config
936 .postgres_cache_database_config
937 .as_ref()
938 .ok_or_else(|| anyhow::anyhow!("No Postgres cache database is configured"))?;
939 BlockchainCacheDatabase::connect(options.clone().into())
940 .await
941 .context("failed to connect to the execution database")
942 }
943
944 fn load_payload_keys(&self) -> anyhow::Result<Option<PayloadKeySet>> {
945 PayloadKeySet::load(
946 self.config.payload_key_env.as_deref(),
947 &self.config.payload_key_retired_env,
948 self.config.payload_deployment_id.as_deref(),
949 )
950 }
951
952 fn payload_policy(&self) -> PayloadPolicy {
953 PayloadPolicy {
954 chain_id: self.chain.chain_id,
955 signer: self.wallet_address,
956 gas_limit: self.config.gas_limit,
957 max_fee_per_gas: self.config.max_fee_per_gas_wei,
958 }
959 }
960
961 fn transaction_executor(&self) -> anyhow::Result<TransactionExecutor> {
962 let database = self.cache.database.clone().ok_or_else(|| {
963 anyhow::anyhow!("No durable store configured; refusing to submit a transaction")
964 })?;
965 let signer = self
966 .signer
967 .clone()
968 .ok_or_else(|| anyhow::anyhow!("Signer not initialized; connect the client first"))?;
969 let payload_keys = self.payload_keys.clone().ok_or_else(|| {
970 anyhow::anyhow!("Protected payload keys are not initialized; connect the client first")
971 })?;
972 let verification_config = self
973 .config
974 .verification
975 .as_ref()
976 .expect("verification config validated at construction");
977 let identities = std::iter::once(&verification_config.authoritative)
978 .chain(
979 verification_config
980 .verifiers
981 .iter()
982 .map(|provider| &provider.identity),
983 )
984 .collect::<Vec<_>>();
985
986 Ok(TransactionExecutor {
987 http_rpc_client: Arc::clone(&self.http_rpc_client),
988 verification: self.verification.clone(),
989 manifest_version: verification_config.manifest_version.clone(),
990 manifest_digest: verification_config.manifest_digest.clone(),
991 deployment_manifest: Arc::new(verification_config.deployment_manifest.clone()),
992 provider_ids: identities
993 .iter()
994 .map(|identity| identity.provider_id.clone())
995 .collect(),
996 operator_ids: identities
997 .iter()
998 .map(|identity| identity.operator_id.clone())
999 .collect(),
1000 failure_domain_ids: identities
1001 .iter()
1002 .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
1003 .collect(),
1004 database,
1005 signer,
1006 payload_keys,
1007 in_flight: Arc::clone(&self.in_flight),
1008 wallet_balance: Arc::clone(&self.wallet_balance),
1009 account_id: self.core.account_id,
1010 wallet_address: self.wallet_address,
1011 chain_id: self.chain.chain_id,
1012 max_fee_per_gas_wei: self.config.max_fee_per_gas_wei,
1013 base_fee_buffer_bps: self.config.base_fee_buffer_bps,
1014 gas_limit: self.config.gas_limit,
1015 gas_buffer_bps: self.config.gas_buffer_bps,
1016 receipt_timeout: receipt_timeout(self.transaction_limits.receipt_timeout_secs),
1017 receipt_max_polls: receipt_max_polls(self.transaction_limits.receipt_timeout_secs),
1018 })
1019 }
1020
1021 fn restore_swap_plan(&self, intent: &ExecutionIntentRow) -> anyhow::Result<SwapPlan> {
1022 let client_order_id = ClientOrderId::new_checked(
1023 intent
1024 .client_order_id
1025 .as_deref()
1026 .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no client order ID"))?,
1027 )?;
1028 let order = self
1029 .core
1030 .cache()
1031 .try_order_owned(&client_order_id)
1032 .with_context(|| {
1033 format!(
1034 "Cannot reconcile swap intent {} because order {client_order_id} is not restored",
1035 intent.id
1036 )
1037 })?;
1038 let instrument_id = InstrumentId::from_str(
1039 intent
1040 .instrument_id
1041 .as_deref()
1042 .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no instrument ID"))?,
1043 )?;
1044 anyhow::ensure!(
1045 order.instrument_id() == instrument_id,
1046 "Persisted swap instrument {instrument_id} does not match restored order instrument {}",
1047 order.instrument_id()
1048 );
1049 anyhow::ensure!(
1050 intent.trader_id.as_deref() == Some(order.trader_id().as_str()),
1051 "Persisted swap trader does not match restored order"
1052 );
1053 anyhow::ensure!(
1054 intent.strategy_id.as_deref() == Some(order.strategy_id().as_str()),
1055 "Persisted swap strategy does not match restored order"
1056 );
1057 anyhow::ensure!(
1058 intent.account_id.as_deref() == Some(self.core.account_id.as_str()),
1059 "Persisted swap account does not match execution client account"
1060 );
1061
1062 let pool = self.resolve_pool(&instrument_id)?;
1063 let pool_address = Address::from_str(
1064 intent
1065 .pool_address
1066 .as_deref()
1067 .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no pool address"))?,
1068 )?;
1069 anyhow::ensure!(
1070 pool.address == pool_address,
1071 "Persisted pool {pool_address} does not match restored pool {}",
1072 pool.address
1073 );
1074 let amount_in = U256::from_str(
1075 intent
1076 .amount_in
1077 .as_deref()
1078 .ok_or_else(|| anyhow::anyhow!("Persisted swap intent has no input amount"))?,
1079 )?;
1080 let fee = U24::try_from(
1081 pool.fee
1082 .ok_or_else(|| anyhow::anyhow!("Restored pool {instrument_id} has no fee"))?,
1083 )?;
1084 let quote_token = pool.get_quote_token();
1085 let quote_currency = Currency::new_checked(
1086 "e_token.symbol,
1087 quote_token.decimals,
1088 0,
1089 "e_token.name,
1090 CurrencyType::Crypto,
1091 )?;
1092 let (token_in, token_out) = swap_token_pair(
1093 order.order_side(),
1094 pool.get_base_token().address,
1095 quote_token.address,
1096 )?;
1097 let factory = self.uniswap_v3_factory()?;
1098 anyhow::ensure!(
1099 pool.dex.factory == factory,
1100 "Restored pool {instrument_id} references factory {}, expected registered factory {factory}",
1101 pool.dex.factory
1102 );
1103
1104 Ok(SwapPlan {
1105 order,
1106 quote_currency,
1107 pool,
1108 instrument_id,
1109 pool_address,
1110 router: Address::from_str(&intent.transaction_to)?,
1111 factory,
1112 weth: self.weth_address,
1113 token_in,
1114 token_out,
1115 fee,
1116 amount_in,
1117 min_amount_out: U256::ZERO,
1118 slippage_bps: 0,
1119 quote_spend_ceiling: None,
1120 profiler_position: None,
1121 })
1122 }
1123
1124 async fn reconcile_unresolved_execution(&self) -> anyhow::Result<()> {
1125 let database = self.cache.database.clone().ok_or_else(|| {
1126 anyhow::anyhow!("No durable store configured for execution reconciliation")
1127 })?;
1128 let _payload_lease = database
1129 .acquire_execution_payload_lease(self.payload_keys.as_deref().ok_or_else(|| {
1130 anyhow::anyhow!("Protected payload keys are required for execution recovery")
1131 })?)
1132 .await?;
1133 let wallet_address = self.wallet_address.to_string();
1134 anyhow::ensure!(
1135 !database
1136 .has_recoverable_signed_execution(self.chain.chain_id, &wallet_address)
1137 .await?,
1138 "A recoverable execution for wallet {} retains signed transaction bytes; refusing to reuse its nonce without explicit recovery",
1139 self.wallet_address
1140 );
1141 let Some(intent) = database
1142 .get_active_execution_intent(self.chain.chain_id, &wallet_address)
1143 .await?
1144 else {
1145 return Ok(());
1146 };
1147 anyhow::ensure!(
1148 intent.schema_version == crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
1149 "Execution intent {} uses unsupported schema version {}",
1150 intent.id,
1151 intent.schema_version
1152 );
1153
1154 if intent.status == "prepared" {
1155 database
1156 .mark_execution_intent_recoverable(intent.id)
1157 .await?;
1158 release_preparing_slot(&self.in_flight);
1159 return Ok(());
1160 }
1161
1162 let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
1163 anyhow::anyhow!(
1164 "Execution intent {} has unknown purpose {}",
1165 intent.id,
1166 intent.purpose
1167 )
1168 })?;
1169 let nonce = intent
1170 .nonce
1171 .ok_or_else(|| anyhow::anyhow!("Active execution intent {} has no nonce", intent.id))?;
1172 *self.in_flight.lock() = Some(InFlightSlot::Recovering(RecoveryTransaction {
1173 intent_id: intent.id,
1174 nonce,
1175 purpose,
1176 }));
1177 let hashes = database.get_execution_transaction_hashes(intent.id).await?;
1178 let current = current_execution_hash(intent.id, &hashes)?;
1179 let tx_hash = B256::from_str(¤t.transaction_hash).with_context(|| {
1180 format!(
1181 "Execution intent {} has invalid transaction hash {}",
1182 intent.id, current.transaction_hash
1183 )
1184 })?;
1185 let policy = PayloadPolicy {
1186 chain_id: self.chain.chain_id,
1187 signer: self.wallet_address,
1188 gas_limit: self.config.gas_limit,
1189 max_fee_per_gas: self.config.max_fee_per_gas_wei,
1190 };
1191 let mut authenticated_payloads = HashMap::new();
1192 let mut current_payload = None;
1193
1194 for hash in &hashes {
1195 anyhow::ensure!(
1196 hash.intent_id == intent.id,
1197 "Persisted transaction row references intent {}, expected {}",
1198 hash.intent_id,
1199 intent.id
1200 );
1201 anyhow::ensure!(
1202 hash.chain_id == self.chain.chain_id,
1203 "Persisted transaction row chain ID {} does not match configured chain ID {}",
1204 hash.chain_id,
1205 self.chain.chain_id
1206 );
1207
1208 if !hash.payload_expected {
1209 anyhow::ensure!(
1210 hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
1211 "Replacement transaction {} unexpectedly retains signed bytes",
1212 hash.transaction_hash
1213 );
1214 continue;
1215 }
1216 let raw_transaction = open_execution_payload(
1217 self.payload_keys
1218 .as_deref()
1219 .expect("payload keys checked above"),
1220 policy,
1221 &intent,
1222 hash,
1223 "recovery",
1224 )
1225 .map_err(|e| {
1226 anyhow::anyhow!(
1227 "Execution intent {} signed transaction {} failed authentication: {e}",
1228 intent.id,
1229 hash.transaction_hash
1230 )
1231 })?;
1232 let authenticated_hash = B256::from_str(&hash.transaction_hash).with_context(|| {
1233 format!(
1234 "Execution intent {} has invalid transaction hash {}",
1235 intent.id, hash.transaction_hash
1236 )
1237 })?;
1238 anyhow::ensure!(
1239 authenticated_payloads
1240 .insert(authenticated_hash, raw_transaction.clone())
1241 .is_none(),
1242 "Execution intent {} has duplicate authenticated transaction hash {}",
1243 intent.id,
1244 hash.transaction_hash
1245 );
1246
1247 if hash.id == current.id {
1248 current_payload = Some(raw_transaction);
1249 }
1250 }
1251 anyhow::ensure!(
1252 !authenticated_payloads.is_empty(),
1253 "Execution intent {} has no persisted signed transaction bytes",
1254 intent.id
1255 );
1256
1257 if intent.status == "broadcast" {
1258 anyhow::ensure!(
1259 current_payload.is_some(),
1260 "Broadcast execution intent {} has no persisted signed transaction bytes",
1261 intent.id
1262 );
1263 }
1264
1265 if intent.status == "signed" {
1266 anyhow::bail!(
1267 "Execution intent {} has a signed transaction {} that was not authorized for broadcast; its nonce remains reserved pending explicit recovery",
1268 intent.id,
1269 tx_hash
1270 );
1271 }
1272
1273 *self.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
1274 intent_id: intent.id,
1275 nonce,
1276 tx_hash,
1277 purpose,
1278 }));
1279
1280 let plan = if purpose == TransactionPurpose::Swap {
1281 Some(self.restore_swap_plan(&intent)?)
1282 } else {
1283 None
1284 };
1285
1286 if let Some(plan) = &plan
1287 && !intent.acknowledgement_emitted
1288 {
1289 if plan.order.ts_submitted().is_none() {
1290 self.emitter.emit_order_submitted(&plan.order);
1291 }
1292 database
1293 .mark_execution_event_emitted(intent.id, "acknowledgement")
1294 .await?;
1295 }
1296
1297 let executor = self.transaction_executor()?;
1298 let mut prepared = PreparedTransaction {
1299 intent_id: intent.id,
1300 created_block: intent.created_block,
1301 nonce,
1302 tx_hash,
1303 raw_tx: current_payload.unwrap_or_default(),
1304 payload_lease: None,
1305 };
1306
1307 let finality_already_committed = matches!(intent.status.as_str(), "finalized" | "reverted");
1308 if !finality_already_committed {
1309 match executor
1310 .authorize_rebroadcast(&prepared, &intent, purpose)
1311 .await?
1312 {
1313 ReconciliationAuthorization::Rebroadcast => {
1314 match executor.broadcast(&prepared).await? {
1315 BroadcastOutcome::Accepted => {}
1316 BroadcastOutcome::Ambiguous(message) => log::warn!("{message}"),
1317 }
1318 }
1319 ReconciliationAuthorization::Retain => {
1320 log::warn!(
1321 "Rebroadcast of transaction {} was suppressed by verified reconciliation state",
1322 prepared.tx_hash
1323 );
1324 }
1325 ReconciliationAuthorization::ScanReplacement(head) => {
1326 let Some((replacement_hash, replacement_payload)) = executor
1327 .scan_canonical_replacement(&intent, nonce, head, &authenticated_payloads)
1328 .await?
1329 else {
1330 log::warn!(
1331 "Canonical replacement scan for intent {} reached its bounded verified window",
1332 intent.id
1333 );
1334 return Ok(());
1335 };
1336 prepared.tx_hash = replacement_hash;
1337 prepared.raw_tx = replacement_payload;
1338 *self.in_flight.lock() =
1339 Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
1340 intent_id: intent.id,
1341 nonce,
1342 tx_hash: replacement_hash,
1343 purpose,
1344 }));
1345 }
1346 }
1347 }
1348 let outcome = if finality_already_committed {
1349 let receipt = verified_value(
1350 executor.verification.verify_receipt(&tx_hash).await,
1351 "persisted terminal receipt",
1352 )?;
1353 let finality = executor
1354 .receipt_is_stably_finalized(&receipt)
1355 .await?
1356 .ok_or_else(|| {
1357 anyhow::anyhow!(
1358 "Persisted terminal transaction {tx_hash} is not stable at the finalized boundary"
1359 )
1360 })?;
1361 let included = IncludedTransaction {
1362 intent_id: intent.id,
1363 nonce,
1364 tx_hash,
1365 block_number: receipt.block_number,
1366 receipt,
1367 finality,
1368 };
1369
1370 if intent.status == "finalized" {
1371 InclusionOutcome::Finalized(included)
1372 } else {
1373 InclusionOutcome::Reverted(included)
1374 }
1375 } else {
1376 executor.await_finality(&prepared).await?
1377 };
1378
1379 match outcome {
1380 InclusionOutcome::Finalized(mut included) => {
1381 let trace_purpose = match (&plan, purpose) {
1382 (Some(plan), TransactionPurpose::Swap) => match plan.order.order_side() {
1383 OrderSide::Sell => "swap_sell",
1384 OrderSide::Buy => "swap_buy",
1385 },
1386 (None, TransactionPurpose::Wrap) => "wrap",
1387 (None, TransactionPurpose::Approve) => "approve",
1388 _ => anyhow::bail!("Restored transaction purpose is inconsistent"),
1389 };
1390 included.finality.decisions.extend(
1391 verify_finalized_transaction(
1392 &included,
1393 &intent,
1394 nonce,
1395 &prepared.raw_tx,
1396 &executor,
1397 trace_purpose,
1398 )
1399 .await?,
1400 );
1401
1402 if let Some(plan) = plan {
1403 let fill = validate_finalized_swap_fill(&plan, &included)?;
1404 let wallet =
1405 load_verified_wallet_after_fill(&plan, &included, &executor).await?;
1406 if !finality_already_committed {
1407 executor
1408 .commit_verified_finality(
1409 &included,
1410 TransactionStatus::Finalized,
1411 &wallet.decisions,
1412 )
1413 .await?;
1414 }
1415 complete_finalized_swap(
1416 &plan,
1417 intent.id,
1418 included.tx_hash,
1419 fill,
1420 wallet,
1421 &executor,
1422 &self.emitter,
1423 )
1424 .await?;
1425 executor.release_slot();
1426 } else {
1427 let postconditions = self
1428 .verify_recovered_operator_transaction(
1429 &intent, purpose, &included, &executor,
1430 )
1431 .await?;
1432
1433 if !finality_already_committed {
1434 executor
1435 .commit_verified_finality(
1436 &included,
1437 TransactionStatus::Finalized,
1438 &postconditions,
1439 )
1440 .await?;
1441 }
1442 database
1443 .mark_execution_event_emitted(intent.id, "terminal")
1444 .await?;
1445 executor.release_slot();
1446 }
1447 }
1448 InclusionOutcome::Reverted(mut included) => {
1449 let trace_purpose = match (&plan, purpose) {
1450 (Some(plan), TransactionPurpose::Swap) => match plan.order.order_side() {
1451 OrderSide::Sell => "swap_sell",
1452 OrderSide::Buy => "swap_buy",
1453 },
1454 (None, TransactionPurpose::Wrap) => "wrap",
1455 (None, TransactionPurpose::Approve) => "approve",
1456 _ => anyhow::bail!("Restored transaction purpose is inconsistent"),
1457 };
1458 included.finality.decisions.extend(
1459 verify_finalized_transaction(
1460 &included,
1461 &intent,
1462 nonce,
1463 &prepared.raw_tx,
1464 &executor,
1465 trace_purpose,
1466 )
1467 .await?,
1468 );
1469
1470 if !finality_already_committed {
1471 executor
1472 .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
1473 .await?;
1474 }
1475
1476 if let Some(plan) = plan
1477 && plan.order.status() != OrderStatus::Rejected
1478 {
1479 send_reverted_order(&self.emitter, &plan.order, &included)?;
1480 }
1481 database
1482 .mark_execution_event_emitted(intent.id, "terminal")
1483 .await?;
1484 executor.release_slot();
1485 }
1486 InclusionOutcome::Pending(message) => log::warn!("{message}"),
1487 }
1488 Ok(())
1489 }
1490
1491 async fn verify_recovered_operator_transaction(
1497 &self,
1498 intent: &ExecutionIntentRow,
1499 purpose: TransactionPurpose,
1500 included: &IncludedTransaction,
1501 executor: &TransactionExecutor,
1502 ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
1503 let (to, input, value) = persisted_call_fields(intent)?;
1504
1505 match purpose {
1506 TransactionPurpose::Wrap => {
1507 verify_wrap_balance_increase(executor, &to, value, included).await
1508 }
1509 TransactionPurpose::Approve => {
1510 let call = ERC20::approveCall::abi_decode(&input)
1511 .with_context(|| "persisted approve calldata is invalid")?;
1512 verify_approve_allowance(executor, &to, &call.spender, call.amount, included).await
1513 }
1514 TransactionPurpose::Swap => {
1515 unreachable!("swap intents restore a swap plan")
1516 }
1517 }
1518 }
1519
1520 fn ensure_transaction_ready(&self, purpose: TransactionPurpose) -> anyhow::Result<()> {
1521 if !self.core.is_connected() {
1522 anyhow::bail!("Blockchain execution client is not connected");
1523 }
1524
1525 {
1526 let slot = self.in_flight.lock();
1527 if let Some(in_flight) = *slot {
1528 return Err(in_flight_limit_error(&in_flight));
1529 }
1530 }
1531
1532 if !self.cache.has_database() {
1533 anyhow::bail!(
1534 "No durable store configured; refusing to submit a {} transaction",
1535 purpose.as_str()
1536 );
1537 }
1538 Ok(())
1539 }
1540
1541 fn prepare_swap(&self, cmd: &SubmitOrder, order: &OrderAny) -> anyhow::Result<SwapPlan> {
1548 let instrument_id = order.instrument_id();
1549 let pool = self.resolve_pool(&instrument_id)?;
1550
1551 if order.order_type() != OrderType::Market {
1552 anyhow::bail!(
1553 "Unsupported order type {}; only Market is supported",
1554 order.order_type()
1555 );
1556 }
1557
1558 if !matches!(order.order_side(), OrderSide::Buy | OrderSide::Sell) {
1559 anyhow::bail!(
1560 "Unsupported order side {}; only Buy and Sell are supported",
1561 order.order_side()
1562 );
1563 }
1564
1565 if order.is_quote_quantity() {
1566 anyhow::bail!(
1567 "Quote-denominated quantities are not supported; quantity must be denominated in the base token"
1568 );
1569 }
1570
1571 let fee = pool
1572 .fee
1573 .ok_or_else(|| anyhow::anyhow!("Pool {instrument_id} has no fee tier"))?;
1574 let fee = U24::try_from(fee)
1575 .map_err(|_| anyhow::anyhow!("Pool {instrument_id} fee {fee} exceeds uint24"))?;
1576
1577 let base_token = pool.get_base_token();
1578 let quote_token = pool.get_quote_token();
1579 let quote_currency = Currency::new_checked(
1580 "e_token.symbol,
1581 quote_token.decimals,
1582 0,
1583 "e_token.name,
1584 CurrencyType::Crypto,
1585 )?;
1586 let (token_in, token_out) =
1587 swap_token_pair(order.order_side(), base_token.address, quote_token.address)?;
1588
1589 if !self
1590 .transaction_limits
1591 .allowed_token_pairs
1592 .contains(&(token_in, token_out))
1593 {
1594 anyhow::bail!(
1595 "Token pair {token_in} -> {token_out} is not in the `allowed_token_pairs` allowlist"
1596 );
1597 }
1598
1599 let base_amount = quantity_to_raw_amount(order.quantity(), base_token.decimals)?;
1600 if base_amount > U256::from(self.transaction_limits.max_order_amount) {
1601 anyhow::bail!(
1602 "Order amount {base_amount} exceeds the configured `max_order_amount` {}",
1603 self.transaction_limits.max_order_amount
1604 );
1605 }
1606
1607 let slippage_bps = match cmd
1608 .params
1609 .as_ref()
1610 .and_then(|params| params.get_u64("slippage_bps"))
1611 {
1612 Some(value) => u32::try_from(value).map_err(|_| {
1613 anyhow::anyhow!("slippage_bps parameter {value} exceeds the u32 range")
1614 })?,
1615 None => self.transaction_limits.slippage_bps,
1616 };
1617
1618 if slippage_bps > self.transaction_limits.max_slippage_bps {
1619 anyhow::bail!(
1620 "Slippage {slippage_bps} bps exceeds the configured `max_slippage_bps` {}",
1621 self.transaction_limits.max_slippage_bps
1622 );
1623 }
1624
1625 let quote_spend_ceiling = if order.order_side() == OrderSide::Buy {
1626 let ceiling = self
1627 .transaction_limits
1628 .quote_spend_limits
1629 .get(&(token_in, token_out))
1630 .ok_or_else(|| {
1631 anyhow::anyhow!(
1632 "No `quote_spend_limits` entry for BUY token pair {token_in} -> {token_out}"
1633 )
1634 })?;
1635 anyhow::ensure!(
1636 ceiling.spend_token == quote_token.address,
1637 "Quote spend limit for {token_in} -> {token_out} is denominated in {}, expected quote token {}",
1638 ceiling.spend_token,
1639 quote_token.address
1640 );
1641 anyhow::ensure!(
1642 ceiling.spend_token_decimals == quote_token.decimals,
1643 "Quote spend limit for token {} uses {} decimals, expected pool quote-token decimals {}",
1644 ceiling.spend_token,
1645 ceiling.spend_token_decimals,
1646 quote_token.decimals
1647 );
1648 Some(ceiling)
1649 } else {
1650 None
1651 };
1652
1653 let profiler = self
1654 .core
1655 .cache()
1656 .pool_profiler(&instrument_id)
1657 .cloned()
1658 .ok_or_else(|| {
1659 anyhow::anyhow!(
1660 "No pool profiler for {instrument_id}; an active data subscription is required to quote the swap"
1661 )
1662 })?;
1663
1664 if !profiler.is_initialized {
1665 anyhow::bail!("Pool profiler for {instrument_id} is not initialized");
1666 }
1667 let profiler_position = profiler.last_processed_event.clone().ok_or_else(|| {
1668 anyhow::anyhow!("Pool profiler for {instrument_id} has processed no events")
1669 })?;
1670
1671 let zero_for_one = token_in == pool.token0.address;
1672 let (amount_in, quoted_amount_out) = match order.order_side() {
1673 OrderSide::Sell => {
1674 let quote = profiler
1675 .swap_exact_in(base_amount, zero_for_one, None)
1676 .map_err(|e| anyhow::anyhow!("Swap quote failed for {instrument_id}: {e}"))?;
1677 let amount_filled = if zero_for_one {
1678 quote.amount0
1679 } else {
1680 quote.amount1
1681 };
1682
1683 if amount_filled != I256::from(base_amount) {
1684 anyhow::bail!(
1685 "Local quote for {instrument_id} filled {amount_filled} of the {base_amount} order amount; pool liquidity cannot fill the order"
1686 );
1687 }
1688 (base_amount, exact_output_amount("e, zero_for_one)?)
1689 }
1690 OrderSide::Buy => {
1691 let quote = profiler
1692 .swap_exact_out(base_amount, zero_for_one, None)
1693 .map_err(|e| anyhow::anyhow!("Swap quote failed for {instrument_id}: {e}"))?;
1694 let amount_in = quote.get_input_amount();
1695 if amount_in.is_zero() {
1696 anyhow::bail!("Local quote for {instrument_id} produced a zero quote input");
1697 }
1698 let ceiling = quote_spend_ceiling.ok_or_else(|| {
1699 anyhow::anyhow!(
1700 "No `quote_spend_limits` entry for BUY token pair {token_in} -> {token_out}"
1701 )
1702 })?;
1703
1704 if amount_in > ceiling.max_amount {
1705 anyhow::bail!(
1706 "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits` maximum {} for {token_in} -> {token_out}",
1707 ceiling.max_amount
1708 );
1709 }
1710 (amount_in, base_amount)
1711 }
1712 };
1713 let min_amount_out = derive_min_amount_out(quoted_amount_out, slippage_bps)?;
1714
1715 self.ensure_transaction_ready(TransactionPurpose::Swap)?;
1716
1717 if self.signer.is_none() {
1718 anyhow::bail!("Signer not initialized; connect the client first");
1719 }
1720
1721 let pool_address = pool.address;
1722 let factory = self.uniswap_v3_factory()?;
1723 anyhow::ensure!(
1724 pool.dex.factory == factory,
1725 "Pool {instrument_id} references factory {}, expected registered factory {factory}",
1726 pool.dex.factory
1727 );
1728
1729 Ok(SwapPlan {
1730 order: order.clone(),
1731 pool,
1732 quote_currency,
1733 instrument_id,
1734 pool_address,
1735 router: self.router_addresses[0],
1736 factory,
1737 weth: self.weth_address,
1738 token_in,
1739 token_out,
1740 fee,
1741 amount_in,
1742 min_amount_out,
1743 slippage_bps,
1744 quote_spend_ceiling: quote_spend_ceiling.copied(),
1745 profiler_position: Some(profiler_position),
1746 })
1747 }
1748}
1749
1750#[derive(Debug, Clone, PartialEq, Eq)]
1752pub struct PayloadStorageCheck {
1753 pub protected: bool,
1755 pub deployment_id: Option<String>,
1757 pub plaintext_rows: u64,
1759 pub original_rows: u64,
1761 pub replacement_rows: u64,
1763 pub authenticated_rows: u64,
1765 pub key_ids: Vec<String>,
1767 pub read_roles: Vec<String>,
1769}
1770
1771impl From<ExecutionPayloadCheck> for PayloadStorageCheck {
1772 fn from(value: ExecutionPayloadCheck) -> Self {
1773 Self {
1774 protected: value.protected,
1775 deployment_id: value.deployment_id,
1776 plaintext_rows: value.plaintext_rows,
1777 original_rows: value.original_rows,
1778 replacement_rows: value.replacement_rows,
1779 authenticated_rows: value.authenticated_rows,
1780 key_ids: value.key_ids,
1781 read_roles: value.read_roles,
1782 }
1783 }
1784}
1785
1786#[derive(Debug, Clone, Copy)]
1788struct InFlightTransaction {
1789 intent_id: i64,
1790 nonce: u64,
1791 tx_hash: B256,
1792 purpose: TransactionPurpose,
1793}
1794
1795#[derive(Debug, Clone, Copy)]
1796struct RecoveryTransaction {
1797 intent_id: i64,
1798 nonce: u64,
1799 purpose: TransactionPurpose,
1800}
1801
1802#[derive(Debug, Clone, Copy)]
1810enum InFlightSlot {
1811 Preparing(TransactionPurpose),
1813 Recovering(RecoveryTransaction),
1815 AwaitingFinality(InFlightTransaction),
1817}
1818
1819#[derive(Debug, Clone)]
1820struct IncludedTransaction {
1821 intent_id: i64,
1822 nonce: u64,
1823 tx_hash: B256,
1824 block_number: u64,
1825 receipt: RpcTransactionReceipt,
1826 finality: StableFinality,
1827}
1828
1829#[derive(Debug, Clone)]
1830struct StableFinality {
1831 decisions: Vec<ExecutionVerificationDecision>,
1832 inclusion_header: ExecutionVerifiedHeader,
1833 finalized_headers: Vec<ExecutionVerifiedHeader>,
1834}
1835
1836#[derive(Debug, Clone, Copy)]
1837enum TransactionAuthorization {
1838 Wrap {
1839 weth: Address,
1840 },
1841 Approve {
1842 token: Address,
1843 router: Address,
1844 amount: U256,
1845 },
1846}
1847
1848fn in_flight_limit_error(slot: &InFlightSlot) -> anyhow::Error {
1850 match slot {
1851 InFlightSlot::Preparing(purpose) => anyhow::anyhow!(
1852 "A {} transaction is being prepared; at most one transaction can be in flight",
1853 purpose.as_str()
1854 ),
1855 InFlightSlot::Recovering(recovery) => anyhow::anyhow!(
1856 "Execution intent {} ({}, nonce {}) retains signer ownership pending recovery; at most one transaction can be in flight",
1857 recovery.intent_id,
1858 recovery.purpose.as_str(),
1859 recovery.nonce
1860 ),
1861 InFlightSlot::AwaitingFinality(in_flight) => anyhow::anyhow!(
1862 "Transaction {} (intent {}, {}, nonce {}) is still awaiting finality; at most one transaction can be in flight",
1863 in_flight.tx_hash,
1864 in_flight.intent_id,
1865 in_flight.purpose.as_str(),
1866 in_flight.nonce
1867 ),
1868 }
1869}
1870
1871fn release_preparing_slot(in_flight: &Mutex<Option<InFlightSlot>>) {
1876 let mut slot = in_flight.lock();
1877 if matches!(*slot, Some(InFlightSlot::Preparing(_))) {
1878 *slot = None;
1879 }
1880}
1881
1882fn release_preparing_if_reservation_not_committed(
1883 in_flight: &Mutex<Option<InFlightSlot>>,
1884 error: &anyhow::Error,
1885) {
1886 if reservation_failure_proven_not_committed(error) {
1887 release_preparing_slot(in_flight);
1888 }
1889}
1890
1891#[derive(Debug)]
1892struct TransactionLimits {
1893 allowed_token_pairs: HashSet<(Address, Address)>,
1894 quote_spend_limits: HashMap<(Address, Address), QuoteSpendCeiling>,
1895 slippage_bps: u32,
1896 max_slippage_bps: u32,
1897 max_order_amount: u64,
1898 deadline_seconds: u64,
1899 max_quote_age_blocks: u64,
1900 receipt_timeout_secs: u64,
1901}
1902
1903#[derive(Debug, Clone, Copy)]
1904struct QuoteSpendCeiling {
1905 spend_token: Address,
1906 spend_token_decimals: u8,
1907 max_amount: U256,
1908}
1909
1910struct PreparedTransaction {
1912 intent_id: i64,
1913 created_block: u64,
1914 nonce: u64,
1915 tx_hash: B256,
1916 raw_tx: Vec<u8>,
1917 payload_lease: Option<ExecutionPayloadLease>,
1918}
1919
1920impl Debug for PreparedTransaction {
1921 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1922 f.debug_struct(stringify!(PreparedTransaction))
1923 .field("intent_id", &self.intent_id)
1924 .field("created_block", &self.created_block)
1925 .field("nonce", &self.nonce)
1926 .field("tx_hash", &self.tx_hash)
1927 .field("raw_tx", &"<redacted>")
1928 .field(
1929 "payload_lease",
1930 &self.payload_lease.as_ref().map(|_| "held"),
1931 )
1932 .finish()
1933 }
1934}
1935
1936#[derive(Debug)]
1938enum BroadcastOutcome {
1939 Accepted,
1941 Ambiguous(String),
1943}
1944
1945#[derive(Debug)]
1947enum InclusionOutcome {
1948 Finalized(IncludedTransaction),
1950 Reverted(IncludedTransaction),
1952 Pending(String),
1955}
1956
1957enum ReconciliationAuthorization {
1958 Rebroadcast,
1959 Retain,
1960 ScanReplacement(VerifiedBlockHeader),
1961}
1962
1963fn verified_value<T>(outcome: VerificationOutcome<T>, context: &str) -> anyhow::Result<T> {
1964 required_verification(outcome, context).map(|verified| verified.value)
1965}
1966
1967fn required_verification<T>(
1968 outcome: VerificationOutcome<T>,
1969 context: &str,
1970) -> anyhow::Result<Verified<T>> {
1971 match outcome {
1972 VerificationOutcome::Verified(verified) => Ok(verified),
1973 VerificationOutcome::Disagreement(_) => {
1974 anyhow::bail!("{context} verification disagreed")
1975 }
1976 VerificationOutcome::Unavailable(_) => {
1977 anyhow::bail!("{context} verification is unavailable")
1978 }
1979 VerificationOutcome::Retryable(_) => {
1980 anyhow::bail!("{context} verification is retryable")
1981 }
1982 VerificationOutcome::LocallyInvalid(_) => {
1983 anyhow::bail!("{context} verification is locally invalid")
1984 }
1985 }
1986}
1987
1988fn validate_transaction_authorization(
1989 authorization: Option<&TransactionAuthorization>,
1990 to: Address,
1991 value: U256,
1992 input: &[u8],
1993) -> anyhow::Result<()> {
1994 match authorization {
1995 None => Ok(()),
1996 Some(TransactionAuthorization::Wrap { weth }) => {
1997 anyhow::ensure!(
1998 to == *weth && !value.is_zero() && input == WETH9::depositCall::SELECTOR,
1999 "Wrap authorization does not match the transaction call"
2000 );
2001 Ok(())
2002 }
2003 Some(TransactionAuthorization::Approve {
2004 token,
2005 router,
2006 amount,
2007 }) => {
2008 let expected = ERC20::approveCall {
2009 spender: *router,
2010 amount: *amount,
2011 }
2012 .abi_encode();
2013 anyhow::ensure!(
2014 to == *token && value.is_zero() && input == expected,
2015 "Approve authorization does not match the transaction call"
2016 );
2017 Ok(())
2018 }
2019 }
2020}
2021
2022fn verification_decision<T>(
2023 verified: &Verified<T>,
2024 height_start: Option<u64>,
2025 height_end: Option<u64>,
2026) -> ExecutionVerificationDecision {
2027 ExecutionVerificationDecision {
2028 read_class: verified.read.as_str(),
2029 height_start,
2030 height_end,
2031 normalized_value_digest: verified.normalized_value_digest.to_string(),
2032 }
2033}
2034
2035fn parse_verified_header(header: &ExecutionVerifiedHeader) -> anyhow::Result<VerifiedBlockHeader> {
2036 Ok(VerifiedBlockHeader {
2037 number: header.number,
2038 hash: B256::from_str(&header.hash).context("Durable finalized header hash is invalid")?,
2039 parent_hash: B256::from_str(&header.parent_hash)
2040 .context("Durable finalized parent hash is invalid")?,
2041 timestamp: header.timestamp,
2042 base_fee_per_gas: header.base_fee_per_gas,
2043 })
2044}
2045
2046fn durable_verified_header(header: &VerifiedBlockHeader) -> ExecutionVerifiedHeader {
2047 ExecutionVerifiedHeader {
2048 number: header.number,
2049 hash: header.hash.to_string(),
2050 parent_hash: header.parent_hash.to_string(),
2051 timestamp: header.timestamp,
2052 base_fee_per_gas: header.base_fee_per_gas,
2053 }
2054}
2055
2056#[derive(Debug, Clone)]
2063struct TransactionExecutor {
2064 http_rpc_client: Arc<BlockchainHttpRpcClient>,
2065 verification: VerificationCoordinator,
2066 manifest_version: String,
2067 manifest_digest: String,
2068 deployment_manifest: Arc<BlockchainDeploymentManifest>,
2069 provider_ids: Vec<String>,
2070 operator_ids: Vec<String>,
2071 failure_domain_ids: Vec<String>,
2072 database: BlockchainCacheDatabase,
2073 signer: Arc<PrivateKeySigner>,
2074 payload_keys: Arc<PayloadKeySet>,
2075 in_flight: Arc<Mutex<Option<InFlightSlot>>>,
2076 wallet_balance: Arc<Mutex<WalletBalance>>,
2077 account_id: AccountId,
2078 wallet_address: Address,
2079 chain_id: u32,
2080 max_fee_per_gas_wei: u64,
2081 base_fee_buffer_bps: u32,
2082 gas_limit: u64,
2083 gas_buffer_bps: u32,
2084 receipt_timeout: Duration,
2085 receipt_max_polls: u32,
2086}
2087
2088impl TransactionExecutor {
2089 async fn transact(
2095 &self,
2096 to: Address,
2097 value: U256,
2098 input: Bytes,
2099 purpose: TransactionPurpose,
2100 client_order_id: Option<ClientOrderId>,
2101 authorization: TransactionAuthorization,
2102 ) -> anyhow::Result<IncludedTransaction> {
2103 self.claim_slot(purpose)?;
2104 let now_unix_secs = current_unix_secs()?;
2105 let decision_header = match required_verification(
2106 self.verification
2107 .verify_decision_header(now_unix_secs)
2108 .await,
2109 "operator decision header",
2110 ) {
2111 Ok(header) => header,
2112 Err(e) => {
2113 release_preparing_slot(&self.in_flight);
2114 return Err(e);
2115 }
2116 };
2117 let created_block = decision_header.value.number;
2118 let intent = ExecutionIntentInsert {
2119 chain_id: self.chain_id,
2120 wallet_address: self.wallet_address.to_string(),
2121 purpose: purpose.as_str().to_string(),
2122 client_order_id: client_order_id.map(|id| id.to_string()),
2123 trader_id: None,
2124 strategy_id: None,
2125 account_id: None,
2126 instrument_id: None,
2127 pool_address: None,
2128 transaction_to: to.to_string(),
2129 transaction_input: hex::encode_prefixed(&input),
2130 transaction_value: value.to_string(),
2131 amount_in: None,
2132 created_block,
2133 };
2134 let intent = match self.database.reserve_execution_intent(&intent).await {
2135 Ok(intent) => intent,
2136 Err(e) => {
2137 release_preparing_if_reservation_not_committed(&self.in_flight, &e);
2138 return Err(e);
2139 }
2140 };
2141 let prepared = match self
2142 .prepare_and_sign(
2143 intent.id,
2144 intent.created_block,
2145 to,
2146 value,
2147 input,
2148 &authorization,
2149 decision_header,
2150 )
2151 .await
2152 {
2153 Ok(prepared) => prepared,
2154 Err(e) => {
2155 if self
2156 .database
2157 .mark_execution_intent_recoverable(intent.id)
2158 .await
2159 .is_ok()
2160 {
2161 release_preparing_slot(&self.in_flight);
2162 }
2163 return Err(e);
2164 }
2165 };
2166 self.fill_and_persist(&prepared, purpose).await?;
2167
2168 match self.broadcast(&prepared).await? {
2169 BroadcastOutcome::Accepted => {}
2170 BroadcastOutcome::Ambiguous(message) => log::warn!("{message}"),
2171 }
2172
2173 match self.await_finality(&prepared).await? {
2174 InclusionOutcome::Finalized(mut included) => {
2175 included.finality.decisions.extend(
2176 verify_finalized_transaction(
2177 &included,
2178 &intent,
2179 prepared.nonce,
2180 &prepared.raw_tx,
2181 self,
2182 purpose.as_str(),
2183 )
2184 .await?,
2185 );
2186 Ok(included)
2187 }
2188 InclusionOutcome::Reverted(mut included) => {
2189 included.finality.decisions.extend(
2190 verify_finalized_transaction(
2191 &included,
2192 &intent,
2193 prepared.nonce,
2194 &prepared.raw_tx,
2195 self,
2196 purpose.as_str(),
2197 )
2198 .await?,
2199 );
2200 self.commit_verified_finality(&included, TransactionStatus::Reverted, &[])
2201 .await?;
2202 self.database
2203 .mark_execution_event_emitted(prepared.intent_id, "terminal")
2204 .await?;
2205 self.release_slot();
2206 anyhow::bail!("Transaction {} reverted on-chain", included.tx_hash)
2207 }
2208 InclusionOutcome::Pending(message) => anyhow::bail!(message),
2209 }
2210 }
2211
2212 fn claim_slot(&self, purpose: TransactionPurpose) -> anyhow::Result<()> {
2215 let mut slot = self.in_flight.lock();
2216 if let Some(in_flight) = *slot {
2217 return Err(in_flight_limit_error(&in_flight));
2218 }
2219 *slot = Some(InFlightSlot::Preparing(purpose));
2220 Ok(())
2221 }
2222
2223 #[expect(
2226 clippy::too_many_arguments,
2227 reason = "Security-critical transaction fields stay explicit at the signing boundary"
2228 )]
2229 async fn prepare_and_sign(
2230 &self,
2231 intent_id: i64,
2232 created_block: u64,
2233 to: Address,
2234 value: U256,
2235 input: Bytes,
2236 authorization: &TransactionAuthorization,
2237 decision_header: Verified<VerifiedBlockHeader>,
2238 ) -> anyhow::Result<PreparedTransaction> {
2239 self.prepare_and_sign_with_anchors(
2240 intent_id,
2241 created_block,
2242 to,
2243 value,
2244 input,
2245 None,
2246 Some(authorization),
2247 Some(decision_header),
2248 )
2249 .await
2250 }
2251
2252 async fn prepare_and_sign_swap(
2253 &self,
2254 intent_id: i64,
2255 created_block: u64,
2256 to: Address,
2257 value: U256,
2258 input: Bytes,
2259 anchors: &SwapQuoteAnchors,
2260 ) -> anyhow::Result<PreparedTransaction> {
2261 self.prepare_and_sign_with_anchors(
2262 intent_id,
2263 created_block,
2264 to,
2265 value,
2266 input,
2267 Some(anchors),
2268 None,
2269 None,
2270 )
2271 .await
2272 }
2273
2274 #[expect(
2275 clippy::too_many_arguments,
2276 reason = "Security-critical transaction fields and verification anchors stay explicit"
2277 )]
2278 async fn prepare_and_sign_with_anchors(
2279 &self,
2280 intent_id: i64,
2281 created_block: u64,
2282 to: Address,
2283 value: U256,
2284 input: Bytes,
2285 swap_anchors: Option<&SwapQuoteAnchors>,
2286 authorization: Option<&TransactionAuthorization>,
2287 decision_header: Option<Verified<VerifiedBlockHeader>>,
2288 ) -> anyhow::Result<PreparedTransaction> {
2289 let expected_chain_id = u64::from(self.chain_id);
2290 let chain_id_verification = required_verification(
2291 self.verification.verify_chain_id().await,
2292 "pre-sign chain ID",
2293 )?;
2294 let actual_chain_id = chain_id_verification.value;
2295 anyhow::ensure!(
2296 actual_chain_id == expected_chain_id,
2297 "Verified chain ID does not match the transaction chain"
2298 );
2299 let decision_header_verification = if let Some(anchors) = swap_anchors {
2300 required_verification(
2301 self.verification.verify_block(anchors.state.number).await,
2302 "pre-sign swap decision header reread",
2303 )?
2304 } else {
2305 match decision_header {
2306 Some(verified) => verified,
2307 None => {
2308 let now_unix_secs = current_unix_secs()?;
2309 required_verification(
2310 self.verification
2311 .verify_decision_header(now_unix_secs)
2312 .await,
2313 "pre-sign decision header",
2314 )?
2315 }
2316 }
2317 };
2318 let decision_header = decision_header_verification.value;
2319
2320 if let Some(anchors) = swap_anchors {
2321 anyhow::ensure!(
2322 decision_header == anchors.state,
2323 "Verified swap decision header changed before signing"
2324 );
2325 }
2326 let decision_ancestry = self.verify_decision_ancestry(decision_header).await?;
2327 validate_transaction_authorization(authorization, to, value, &input)?;
2328 let deployment_verification = required_verification(
2329 self.verification
2330 .verify_deployment_manifest(&self.deployment_manifest, decision_header.number)
2331 .await,
2332 "pre-sign deployment manifest",
2333 )?;
2334 let authorization_decisions = match authorization {
2335 Some(authorization) => {
2336 self.verify_transaction_authorization(authorization, decision_header.number)
2337 .await?
2338 }
2339 None => Vec::new(),
2340 };
2341 let base_fee_per_gas_wei = decision_header.base_fee_per_gas.ok_or_else(|| {
2342 anyhow::anyhow!(
2343 "Verified decision block {} has no base fee",
2344 decision_header.number
2345 )
2346 })?;
2347 let priority_fee_verification = required_verification(
2348 self.verification.verify_priority_fee().await,
2349 "pre-sign priority fee",
2350 )?;
2351 let priority_fee_per_gas_wei = priority_fee_verification.value;
2352 let (max_fee_per_gas, max_priority_fee_per_gas) = derive_fees(
2353 base_fee_per_gas_wei,
2354 priority_fee_per_gas_wei,
2355 self.base_fee_buffer_bps,
2356 u128::from(self.max_fee_per_gas_wei),
2357 )?;
2358 let gas_estimate_verification = required_verification(
2359 self.verification
2360 .verify_gas_estimate(
2361 &self.wallet_address,
2362 &to,
2363 value,
2364 &input,
2365 decision_header.number,
2366 )
2367 .await,
2368 "pre-sign gas estimate",
2369 )?;
2370 let gas_estimate = gas_estimate_verification.value;
2371 let gas_limit = derive_gas_limit(gas_estimate, self.gas_buffer_bps, self.gas_limit)?;
2372 let max_gas_cost = U256::from(gas_limit)
2373 .checked_mul(U256::from(max_fee_per_gas))
2374 .ok_or_else(|| anyhow::anyhow!("Maximum gas cost overflow"))?;
2375 let max_transaction_cost = value
2376 .checked_add(max_gas_cost)
2377 .ok_or_else(|| anyhow::anyhow!("Maximum transaction cost overflow"))?;
2378 let native_balance_verification = required_verification(
2379 self.verification
2380 .verify_balance(&self.wallet_address, decision_header.number)
2381 .await,
2382 "pre-sign native balance",
2383 )?;
2384 let native_balance = native_balance_verification.value;
2385
2386 if native_balance < max_transaction_cost {
2387 anyhow::bail!(
2388 "Native currency balance {native_balance} wei is below maximum transaction cost {max_transaction_cost} wei"
2389 );
2390 }
2391 let decision_height = Some(decision_header.number);
2392 let mut decisions = vec![
2393 verification_decision(&chain_id_verification, None, None),
2394 verification_decision(
2395 &decision_header_verification,
2396 decision_height,
2397 decision_height,
2398 ),
2399 verification_decision(&deployment_verification, decision_height, decision_height),
2400 verification_decision(&priority_fee_verification, None, None),
2401 verification_decision(&gas_estimate_verification, decision_height, decision_height),
2402 verification_decision(
2403 &native_balance_verification,
2404 decision_height,
2405 decision_height,
2406 ),
2407 ];
2408 decisions.extend(decision_ancestry);
2409 decisions.extend(authorization_decisions);
2410 if let Some(anchors) = swap_anchors {
2411 decisions.extend(self.verify_swap_anchors_before_sign(anchors).await?);
2412 decisions.extend(anchors.precondition_decisions.iter().cloned());
2413 } else {
2414 decisions.extend(self.verify_pre_sign_header_fence(decision_header).await?);
2415 }
2416 let canonical_nonce_verification = required_verification(
2417 self.verification
2418 .verify_transaction_count(&self.wallet_address, decision_header.number)
2419 .await,
2420 "pre-sign canonical nonce reread",
2421 )?;
2422 let pending_nonce_verification = required_verification(
2423 self.verification
2424 .verify_pending_transaction_count(&self.wallet_address)
2425 .await,
2426 "pre-sign pending nonce reread",
2427 )?;
2428 anyhow::ensure!(
2429 pending_nonce_verification.value == canonical_nonce_verification.value,
2430 "Pending nonce does not match the verified canonical nonce"
2431 );
2432 let nonce = canonical_nonce_verification.value;
2433 decisions.push(verification_decision(
2434 &canonical_nonce_verification,
2435 decision_height,
2436 decision_height,
2437 ));
2438 decisions.push(verification_decision(
2439 &pending_nonce_verification,
2440 None,
2441 None,
2442 ));
2443 let tx = build_eip1559_transaction(
2444 expected_chain_id,
2445 nonce,
2446 gas_limit,
2447 max_fee_per_gas,
2448 max_priority_fee_per_gas,
2449 to,
2450 value,
2451 input,
2452 );
2453 let wallet_address = self.wallet_address.to_string();
2454 self.database
2455 .assign_execution_intent_nonce_verified(&ExecutionNonceAssignment {
2456 intent_id,
2457 chain_id: self.chain_id,
2458 wallet_address: &wallet_address,
2459 nonce,
2460 manifest_version: &self.manifest_version,
2461 manifest_digest: &self.manifest_digest,
2462 provider_ids: &self.provider_ids,
2463 operator_ids: &self.operator_ids,
2464 failure_domain_ids: &self.failure_domain_ids,
2465 decisions: &decisions,
2466 })
2467 .await?;
2468 let payload_lease = self
2469 .database
2470 .acquire_execution_payload_lease(&self.payload_keys)
2471 .await?;
2472 let (tx_hash, raw_tx) = sign_eip1559_transaction(tx, &self.signer).await?;
2473
2474 Ok(PreparedTransaction {
2475 intent_id,
2476 created_block,
2477 nonce,
2478 tx_hash,
2479 raw_tx,
2480 payload_lease: Some(payload_lease),
2481 })
2482 }
2483
2484 async fn verify_pre_sign_header_fence(
2485 &self,
2486 target: VerifiedBlockHeader,
2487 ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2488 let checkpoint = required_verification(
2489 self.verification.verify_checkpoint().await,
2490 "pre-sign checkpoint reread",
2491 )?;
2492 let header = required_verification(
2493 self.verification.verify_block(target.number).await,
2494 "pre-sign decision header reread",
2495 )?;
2496 anyhow::ensure!(
2497 header.value == target,
2498 "Decision header changed before signing"
2499 );
2500 Ok(vec![
2501 verification_decision(
2502 &checkpoint,
2503 Some(checkpoint.value.number),
2504 Some(checkpoint.value.number),
2505 ),
2506 verification_decision(&header, Some(target.number), Some(target.number)),
2507 ])
2508 }
2509
2510 async fn verify_decision_ancestry(
2511 &self,
2512 target: VerifiedBlockHeader,
2513 ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2514 let checkpoint = required_verification(
2515 self.verification.verify_checkpoint().await,
2516 "pre-sign checkpoint",
2517 )?;
2518 anyhow::ensure!(
2519 checkpoint.value.number <= target.number,
2520 "Pre-sign decision header precedes the trusted checkpoint"
2521 );
2522 let mut decisions = vec![verification_decision(
2523 &checkpoint,
2524 Some(checkpoint.value.number),
2525 Some(checkpoint.value.number),
2526 )];
2527 let wallet_address = self.wallet_address.to_string();
2528 let position = self
2529 .database
2530 .load_execution_verification_position(
2531 self.chain_id,
2532 &wallet_address,
2533 &self.manifest_version,
2534 &self.manifest_digest,
2535 )
2536 .await?
2537 .ok_or_else(|| anyhow::anyhow!("Execution verification ledger is not initialized"))?;
2538 let durable_tip = parse_verified_header(&position.finalized_tip)?;
2539 anyhow::ensure!(
2540 durable_tip.number >= checkpoint.value.number && durable_tip.number <= target.number,
2541 "Pre-sign decision header does not extend the durable finalized header tip"
2542 );
2543 let durable_tip_verification = required_verification(
2544 self.verification.verify_block(durable_tip.number).await,
2545 "pre-sign durable finalized tip",
2546 )?;
2547 anyhow::ensure!(
2548 durable_tip_verification.value == durable_tip,
2549 "Durable finalized header tip conflicts with independent sources"
2550 );
2551
2552 if durable_tip != checkpoint.value {
2553 decisions.push(verification_decision(
2554 &durable_tip_verification,
2555 Some(durable_tip.number),
2556 Some(durable_tip.number),
2557 ));
2558 }
2559 let mut cursor = durable_tip;
2560 while cursor.number < target.number {
2561 let end = cursor.number.saturating_add(4_096).min(target.number);
2562 let start = cursor.number.saturating_add(1);
2563 let ancestry = required_verification(
2564 self.verification.verify_header_window(cursor, end).await,
2565 "pre-sign decision ancestry",
2566 )?;
2567 cursor = *ancestry
2568 .value
2569 .last()
2570 .expect("nonempty decision ancestry advances the cursor");
2571 decisions.push(verification_decision(&ancestry, Some(start), Some(end)));
2572 }
2573 anyhow::ensure!(
2574 cursor == target,
2575 "Pre-sign decision header conflicts with its trusted ancestry"
2576 );
2577 Ok(decisions)
2578 }
2579
2580 async fn verify_transaction_authorization(
2581 &self,
2582 authorization: &TransactionAuthorization,
2583 block: u64,
2584 ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2585 match *authorization {
2586 TransactionAuthorization::Wrap { weth } => {
2587 let call = ERC20::balanceOfCall {
2588 account: self.wallet_address,
2589 }
2590 .abi_encode();
2591 let balance = required_verification(
2592 self.verification
2593 .verify_decoded_call(None, &weth, U256::ZERO, &call, block, |result| {
2594 ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into)
2595 })
2596 .await,
2597 "pre-sign wrapped token probe",
2598 )?;
2599 Ok(vec![verification_decision(
2600 &balance,
2601 Some(block),
2602 Some(block),
2603 )])
2604 }
2605 TransactionAuthorization::Approve {
2606 token,
2607 router,
2608 amount,
2609 } => {
2610 let allowance_call = ERC20::allowanceCall {
2611 owner: self.wallet_address,
2612 spender: router,
2613 }
2614 .abi_encode();
2615 let allowance = required_verification(
2616 self.verification
2617 .verify_decoded_call(
2618 None,
2619 &token,
2620 U256::ZERO,
2621 &allowance_call,
2622 block,
2623 |result| {
2624 ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into)
2625 },
2626 )
2627 .await,
2628 "pre-sign router allowance",
2629 )?;
2630 anyhow::ensure!(
2631 allowance.value.is_zero() || amount.is_zero(),
2632 "Router allowance for token {token} is already {}; approve zero before setting a new nonzero allowance",
2633 allowance.value
2634 );
2635
2636 let approve_call = ERC20::approveCall {
2637 spender: router,
2638 amount,
2639 }
2640 .abi_encode();
2641 let simulation = required_verification(
2642 self.verification
2643 .verify_decoded_simulation(
2644 &self.wallet_address,
2645 &token,
2646 U256::ZERO,
2647 &approve_call,
2648 block,
2649 |result| {
2650 if result.is_empty() {
2651 Ok(true)
2652 } else {
2653 ERC20::approveCall::abi_decode_returns_validate(result)
2654 .map_err(Into::into)
2655 }
2656 },
2657 )
2658 .await,
2659 "pre-sign approval simulation",
2660 )?;
2661
2662 match &simulation.value {
2663 VerifiedSimulation::Succeeded(true) => {}
2664 VerifiedSimulation::Succeeded(false) => {
2665 anyhow::bail!("ERC-20 approve returned false for token {token}")
2666 }
2667 VerifiedSimulation::Denied => {
2668 anyhow::bail!("ERC-20 approve simulation reverted for token {token}")
2669 }
2670 }
2671 Ok(vec![
2672 verification_decision(&allowance, Some(block), Some(block)),
2673 verification_decision(&simulation, Some(block), Some(block)),
2674 ])
2675 }
2676 }
2677 }
2678
2679 async fn verify_swap_anchors_before_sign(
2680 &self,
2681 anchors: &SwapQuoteAnchors,
2682 ) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
2683 let checkpoint = required_verification(
2684 self.verification.verify_checkpoint().await,
2685 "pre-sign checkpoint reread",
2686 )?;
2687 let watermark = required_verification(
2688 self.verification
2689 .verify_block(anchors.watermark.number)
2690 .await,
2691 "pre-sign profiler watermark reread",
2692 )?;
2693 anyhow::ensure!(
2694 watermark.value == anchors.watermark,
2695 "Pool state header changed before signing"
2696 );
2697 let ancestry = required_verification(
2698 self.verification
2699 .verify_header_window(watermark.value, anchors.state.number)
2700 .await,
2701 "pre-sign profiler ancestry reread",
2702 )?;
2703
2704 if let Some(last) = ancestry.value.last() {
2705 anyhow::ensure!(
2706 *last == anchors.state,
2707 "Swap decision header changed in the pre-sign ancestry reread"
2708 );
2709 } else {
2710 anyhow::ensure!(
2711 watermark.value == anchors.state,
2712 "Empty pre-sign ancestry does not end at the swap decision header"
2713 );
2714 }
2715 let quote = match anchors.quote_kind {
2716 SwapQuoteKind::ExactInput(amount_in) => required_verification(
2717 self.verification
2718 .verify_quote_exact_input_single(
2719 &anchors.quote_contract,
2720 anchors.token_in,
2721 anchors.token_out,
2722 amount_in,
2723 anchors.fee,
2724 anchors.state.number,
2725 )
2726 .await,
2727 "pre-sign exact-input quote reread",
2728 )?,
2729 SwapQuoteKind::ExactOutput(amount_out) => required_verification(
2730 self.verification
2731 .verify_quote_exact_output_single(
2732 &anchors.quote_contract,
2733 anchors.token_in,
2734 anchors.token_out,
2735 amount_out,
2736 anchors.fee,
2737 anchors.state.number,
2738 )
2739 .await,
2740 "pre-sign exact-output quote reread",
2741 )?,
2742 };
2743 anyhow::ensure!(
2744 quote.value == anchors.quote,
2745 "Independent swap quote changed before signing"
2746 );
2747 Ok(vec![
2748 verification_decision(
2749 &checkpoint,
2750 Some(checkpoint.value.number),
2751 Some(checkpoint.value.number),
2752 ),
2753 verification_decision(
2754 &watermark,
2755 Some(anchors.watermark.number),
2756 Some(anchors.watermark.number),
2757 ),
2758 verification_decision(
2759 &ancestry,
2760 Some(anchors.watermark.number),
2761 Some(anchors.state.number),
2762 ),
2763 verification_decision(
2764 "e,
2765 Some(anchors.state.number),
2766 Some(anchors.state.number),
2767 ),
2768 ])
2769 }
2770
2771 async fn fill_and_persist(
2779 &self,
2780 prepared: &PreparedTransaction,
2781 purpose: TransactionPurpose,
2782 ) -> anyhow::Result<()> {
2783 {
2784 let mut slot = self.in_flight.lock();
2785 *slot = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
2786 intent_id: prepared.intent_id,
2787 nonce: prepared.nonce,
2788 tx_hash: prepared.tx_hash,
2789 purpose,
2790 }));
2791 }
2792
2793 let tx_hash = prepared.tx_hash;
2794 let transaction_hash = tx_hash.to_string();
2795 let policy = self.payload_policy();
2796 let intent = self
2797 .database
2798 .get_execution_intent(prepared.intent_id)
2799 .await?;
2800 authenticate_payload_identity(
2801 &prepared.raw_tx,
2802 &intent,
2803 &transaction_hash,
2804 self.chain_id,
2805 policy,
2806 )
2807 .with_context(|| format!("Newly signed transaction {tx_hash} failed authentication"))?;
2808
2809 self.database
2810 .reserve_execution_payload_seal(&self.payload_keys)
2811 .await?;
2812 let context = payload_context_identity(
2813 &intent,
2814 &transaction_hash,
2815 self.chain_id,
2816 self.payload_keys.deployment_id(),
2817 )?;
2818 let envelope = self.payload_keys.seal(&prepared.raw_tx, &context)?;
2819 let row = self
2820 .database
2821 .add_execution_transaction_envelope(
2822 prepared.intent_id,
2823 self.chain_id,
2824 &transaction_hash,
2825 &envelope,
2826 )
2827 .await
2828 .map_err(|e| {
2829 anyhow::anyhow!(
2830 "Failed to persist transaction {tx_hash}: {e}; the in-flight slot stays occupied"
2831 )
2832 })?;
2833 let stored = open_execution_payload(
2834 &self.payload_keys,
2835 policy,
2836 &intent,
2837 &row,
2838 "initial persistence",
2839 )?;
2840 anyhow::ensure!(
2841 stored == prepared.raw_tx,
2842 "Persisted transaction {tx_hash} does not match the signed bytes"
2843 );
2844 Ok(())
2845 }
2846
2847 fn payload_policy(&self) -> PayloadPolicy {
2848 PayloadPolicy {
2849 chain_id: self.chain_id,
2850 signer: self.wallet_address,
2851 gas_limit: self.gas_limit,
2852 max_fee_per_gas: self.max_fee_per_gas_wei,
2853 }
2854 }
2855
2856 async fn authorize_rebroadcast(
2857 &self,
2858 prepared: &PreparedTransaction,
2859 intent: &ExecutionIntentRow,
2860 purpose: TransactionPurpose,
2861 ) -> anyhow::Result<ReconciliationAuthorization> {
2862 let now_unix_secs = current_unix_secs()?;
2863 let decision_header = required_verification(
2864 self.verification
2865 .verify_decision_header(now_unix_secs)
2866 .await,
2867 "rebroadcast decision header",
2868 )?;
2869 let block = decision_header.value.number;
2870 let mut decisions = vec![verification_decision(
2871 &decision_header,
2872 Some(block),
2873 Some(block),
2874 )];
2875 decisions.extend(self.verify_decision_ancestry(decision_header.value).await?);
2876 let deployment = required_verification(
2877 self.verification
2878 .verify_deployment_manifest(&self.deployment_manifest, block)
2879 .await,
2880 "rebroadcast deployment manifest",
2881 )?;
2882 decisions.push(verification_decision(&deployment, Some(block), Some(block)));
2883 let canonical_nonce = required_verification(
2884 self.verification
2885 .verify_transaction_count(&self.wallet_address, block)
2886 .await,
2887 "rebroadcast canonical nonce",
2888 )?;
2889 decisions.push(verification_decision(
2890 &canonical_nonce,
2891 Some(block),
2892 Some(block),
2893 ));
2894 let pending_nonce = required_verification(
2895 self.verification
2896 .verify_reconciliation_pending_transaction_count(
2897 &self.wallet_address,
2898 prepared.nonce,
2899 )
2900 .await,
2901 "rebroadcast pending nonce",
2902 )?;
2903 decisions.push(verification_decision(&pending_nonce, None, None));
2904 let receipt_absence = required_verification(
2905 self.verification
2906 .verify_receipt_absence(&prepared.tx_hash)
2907 .await,
2908 "rebroadcast receipt absence",
2909 )?;
2910 decisions.push(verification_decision(&receipt_absence, None, None));
2911
2912 let next_nonce = prepared
2913 .nonce
2914 .checked_add(1)
2915 .ok_or_else(|| anyhow::anyhow!("Owned signer nonce overflow"))?;
2916 anyhow::ensure!(
2917 (prepared.nonce..=next_nonce).contains(&pending_nonce.value),
2918 "Pending nonce {} is outside the owned reconciliation range {}..={next_nonce}",
2919 pending_nonce.value,
2920 prepared.nonce
2921 );
2922 anyhow::ensure!(
2923 (prepared.nonce..=next_nonce).contains(&canonical_nonce.value),
2924 "Canonical nonce {} is outside the owned reconciliation range {}..={next_nonce}",
2925 canonical_nonce.value,
2926 prepared.nonce
2927 );
2928
2929 if !receipt_absence.value {
2930 self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
2931 .await?;
2932 return Ok(ReconciliationAuthorization::Retain);
2933 }
2934
2935 if canonical_nonce.value == next_nonce {
2936 self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
2937 .await?;
2938 return Ok(ReconciliationAuthorization::ScanReplacement(
2939 decision_header.value,
2940 ));
2941 }
2942
2943 let (to, input, value) = persisted_call_fields(intent)?;
2944 let authorized = match purpose {
2945 TransactionPurpose::Wrap => {
2946 let simulation = required_verification(
2947 self.verification
2948 .verify_decoded_simulation(
2949 &self.wallet_address,
2950 &to,
2951 value,
2952 &input,
2953 block,
2954 |result| Ok(result.is_empty()),
2955 )
2956 .await,
2957 "rebroadcast wrap simulation",
2958 )?;
2959 let authorized = matches!(&simulation.value, VerifiedSimulation::Succeeded(true));
2960 decisions.push(verification_decision(&simulation, Some(block), Some(block)));
2961 authorized
2962 }
2963 TransactionPurpose::Approve => {
2964 let simulation = required_verification(
2965 self.verification
2966 .verify_decoded_simulation(
2967 &self.wallet_address,
2968 &to,
2969 value,
2970 &input,
2971 block,
2972 |result| {
2973 if result.is_empty() {
2974 Ok(true)
2975 } else {
2976 ERC20::approveCall::abi_decode_returns_validate(result)
2977 .map_err(Into::into)
2978 }
2979 },
2980 )
2981 .await,
2982 "rebroadcast approve simulation",
2983 )?;
2984 let authorized = matches!(&simulation.value, VerifiedSimulation::Succeeded(true));
2985 decisions.push(verification_decision(&simulation, Some(block), Some(block)));
2986 authorized
2987 }
2988 TransactionPurpose::Swap => {
2989 let call = UniswapV3SwapRouter::exactInputSingleCall::abi_decode(&input)
2990 .with_context(|| "persisted swap calldata is invalid")?;
2991
2992 if U256::from(decision_header.value.timestamp) > call.params.deadline {
2993 false
2994 } else {
2995 let simulation = required_verification(
2996 self.verification
2997 .verify_decoded_simulation(
2998 &self.wallet_address,
2999 &to,
3000 value,
3001 &input,
3002 block,
3003 |result| {
3004 UniswapV3SwapRouter::exactInputSingleCall::abi_decode_returns(
3005 result,
3006 )
3007 .map_err(Into::into)
3008 },
3009 )
3010 .await,
3011 "rebroadcast swap simulation",
3012 )?;
3013 let authorized = match &simulation.value {
3014 VerifiedSimulation::Succeeded(amount_out) => {
3015 *amount_out >= call.params.amountOutMinimum
3016 }
3017 VerifiedSimulation::Denied => false,
3018 };
3019 decisions.push(verification_decision(&simulation, Some(block), Some(block)));
3020 authorized
3021 }
3022 }
3023 };
3024 self.persist_rebroadcast_decisions(intent.id, prepared.nonce, &decisions)
3025 .await?;
3026 Ok(if authorized {
3027 ReconciliationAuthorization::Rebroadcast
3028 } else {
3029 ReconciliationAuthorization::Retain
3030 })
3031 }
3032
3033 async fn scan_canonical_replacement(
3034 &self,
3035 intent: &ExecutionIntentRow,
3036 nonce: u64,
3037 head: VerifiedBlockHeader,
3038 authenticated_payloads: &HashMap<B256, Vec<u8>>,
3039 ) -> anyhow::Result<Option<(B256, Vec<u8>)>> {
3040 let wallet_address = self.wallet_address.to_string();
3041 let cursor = self
3042 .database
3043 .load_execution_replacement_cursor(
3044 intent.id,
3045 self.chain_id,
3046 &wallet_address,
3047 nonce,
3048 &self.manifest_digest,
3049 )
3050 .await?;
3051 let start = cursor.as_ref().map_or(intent.created_block, |header| {
3052 header.number.saturating_add(1)
3053 });
3054 anyhow::ensure!(
3055 start <= head.number,
3056 "Canonical nonce advanced without an authenticated signer transaction in the scanned canonical range"
3057 );
3058 let scan_range = replacement_scan_range(start, head.number)?;
3059 let end = *scan_range.end();
3060 let mut decisions = Vec::new();
3061 let mut blocks = Vec::new();
3062
3063 if let Some(cursor) = cursor.as_ref() {
3064 let parent = parse_verified_header(cursor)?;
3065 let window = required_verification(
3066 self.verification
3067 .verify_replacement_window(parent, end)
3068 .await,
3069 "canonical replacement window",
3070 )?;
3071 decisions.push(verification_decision(&window, Some(start), Some(end)));
3072 blocks = window.value;
3073 } else {
3074 let start_header = required_verification(
3075 self.verification.verify_block(start).await,
3076 "canonical replacement start header",
3077 )?;
3078 decisions.push(verification_decision(
3079 &start_header,
3080 Some(start),
3081 Some(start),
3082 ));
3083 let start_block = required_verification(
3084 self.verification.verify_replacement_block(start).await,
3085 "canonical replacement start block",
3086 )?;
3087 anyhow::ensure!(
3088 VerifiedBlockHeader::from(start_block.value.clone()) == start_header.value,
3089 "Replacement block conflicts with its canonical header"
3090 );
3091 decisions.push(verification_decision(
3092 &start_block,
3093 Some(start),
3094 Some(start),
3095 ));
3096 blocks.push(start_block.value);
3097
3098 if end > start {
3099 let window = required_verification(
3100 self.verification
3101 .verify_replacement_window(start_header.value, end)
3102 .await,
3103 "canonical replacement window",
3104 )?;
3105 decisions.push(verification_decision(&window, Some(start + 1), Some(end)));
3106 blocks.extend(window.value);
3107 }
3108 }
3109
3110 let scanned_tip = blocks
3111 .last()
3112 .map(|block| VerifiedBlockHeader::from(block.clone()))
3113 .ok_or_else(|| anyhow::anyhow!("Verified replacement scan returned no blocks"))?;
3114 if end == head.number {
3115 anyhow::ensure!(
3116 scanned_tip == head,
3117 "Replacement scan tip conflicts with the verified canonical head"
3118 );
3119 }
3120 let mut candidates = blocks
3121 .iter()
3122 .flat_map(|block| block.transactions.iter())
3123 .filter(|transaction| {
3124 transaction.from == self.wallet_address && transaction.nonce == nonce
3125 });
3126 let candidate = candidates.next();
3127 anyhow::ensure!(
3128 candidates.next().is_none(),
3129 "Canonical replacement scan found duplicate signer-nonce transactions"
3130 );
3131
3132 let finalized_cursor = self
3133 .database
3134 .load_execution_verified_header(
3135 self.chain_id,
3136 &wallet_address,
3137 end,
3138 &self.manifest_digest,
3139 )
3140 .await?;
3141
3142 if let Some(cursor) = finalized_cursor.as_ref() {
3143 anyhow::ensure!(
3144 parse_verified_header(cursor)? == scanned_tip,
3145 "Replacement scan conflicts with the durable finalized header ledger"
3146 );
3147 }
3148
3149 let mut mismatch = None;
3150 let matched = candidate.and_then(|transaction| {
3151 let Some(raw_transaction) = authenticated_payloads.get(&transaction.hash).cloned()
3152 else {
3153 mismatch = Some(anyhow::anyhow!(
3154 "Canonical signer-nonce transaction {} has no authenticated retained payload",
3155 transaction.hash
3156 ));
3157 return None;
3158 };
3159
3160 if let Err(e) = validate_rpc_transaction_matches_payload(transaction, &raw_transaction)
3161 {
3162 mismatch = Some(e.context(format!(
3163 "Canonical signer-nonce transaction {} failed authenticated payload validation",
3164 transaction.hash
3165 )));
3166 return None;
3167 }
3168 Some((transaction.hash, raw_transaction))
3169 });
3170 let matched_hash = matched.as_ref().map(|(hash, _)| hash.to_string());
3171 self.database
3172 .record_execution_replacement_scan(&ExecutionReplacementScan {
3173 intent_id: intent.id,
3174 chain_id: self.chain_id,
3175 wallet_address: &wallet_address,
3176 nonce,
3177 finalized_cursor: finalized_cursor.as_ref(),
3178 matched_transaction_hash: matched_hash.as_deref(),
3179 manifest_version: &self.manifest_version,
3180 manifest_digest: &self.manifest_digest,
3181 provider_ids: &self.provider_ids,
3182 operator_ids: &self.operator_ids,
3183 failure_domain_ids: &self.failure_domain_ids,
3184 decisions: &decisions,
3185 })
3186 .await?;
3187
3188 if let Some(e) = mismatch {
3189 return Err(e);
3190 }
3191
3192 if matched.is_none() && end == head.number {
3193 anyhow::bail!(
3194 "Canonical nonce advanced without an authenticated signer transaction in the canonical range"
3195 );
3196 }
3197 Ok(matched)
3198 }
3199
3200 async fn persist_rebroadcast_decisions(
3201 &self,
3202 intent_id: i64,
3203 nonce: u64,
3204 decisions: &[ExecutionVerificationDecision],
3205 ) -> anyhow::Result<()> {
3206 let wallet_address = self.wallet_address.to_string();
3207 self.database
3208 .record_execution_verification_batch(&ExecutionVerificationBatch {
3209 intent_id,
3210 chain_id: self.chain_id,
3211 wallet_address: &wallet_address,
3212 nonce,
3213 decision_class: "rebroadcast",
3214 manifest_version: &self.manifest_version,
3215 manifest_digest: &self.manifest_digest,
3216 provider_ids: &self.provider_ids,
3217 operator_ids: &self.operator_ids,
3218 failure_domain_ids: &self.failure_domain_ids,
3219 decisions,
3220 })
3221 .await
3222 }
3223
3224 async fn broadcast(&self, prepared: &PreparedTransaction) -> anyhow::Result<BroadcastOutcome> {
3225 let tx_hash = prepared.tx_hash;
3226
3227 self.database
3228 .record_execution_status(
3229 prepared.intent_id,
3230 &tx_hash.to_string(),
3231 TransactionStatus::Broadcast,
3232 None,
3233 None,
3234 None,
3235 None,
3236 None,
3237 )
3238 .await
3239 .map_err(|e| {
3240 anyhow::anyhow!(
3241 "Failed to persist broadcast attempt for transaction {tx_hash}: {e}; the in-flight slot stays occupied"
3242 )
3243 })?;
3244
3245 match self
3246 .http_rpc_client
3247 .send_raw_transaction(&prepared.raw_tx, &tx_hash)
3248 .await
3249 {
3250 Ok(broadcast_hash) => {
3251 if broadcast_hash != tx_hash {
3252 return Ok(BroadcastOutcome::Ambiguous(format!(
3256 "Broadcast of transaction {tx_hash} returned a differing hash {broadcast_hash}; the persisted record reconciles instead of rebroadcasting"
3257 )));
3258 }
3259 Ok(BroadcastOutcome::Accepted)
3260 }
3261 Err(BroadcastError::TimeoutAfterSend) => Ok(BroadcastOutcome::Ambiguous(format!(
3262 "Broadcast of transaction {tx_hash} timed out after send; the persisted record reconciles instead of rebroadcasting"
3263 ))),
3264 Err(BroadcastError::Failed(message)) => {
3265 Ok(BroadcastOutcome::Ambiguous(format!(
3269 "Broadcast of transaction {tx_hash} failed ambiguously ({message}); the persisted record reconciles instead of rebroadcasting"
3270 )))
3271 }
3272 Err(error @ BroadcastError::Rejected { .. }) => {
3273 Ok(BroadcastOutcome::Ambiguous(format!(
3274 "Broadcast of transaction {tx_hash} was rejected ({error}); the signed hash remains occupied until canonical nonce reconciliation"
3275 )))
3276 }
3277 }
3278 }
3279
3280 async fn await_finality(
3282 &self,
3283 prepared: &PreparedTransaction,
3284 ) -> anyhow::Result<InclusionOutcome> {
3285 let tx_hash = prepared.tx_hash;
3286 let deadline = tokio::time::Instant::now() + self.receipt_timeout;
3287
3288 for attempt in 0..self.receipt_max_polls {
3289 if tokio::time::Instant::now() >= deadline {
3290 break;
3291 }
3292
3293 if attempt > 0 {
3294 tokio::time::sleep(RECEIPT_POLL_INTERVAL).await;
3295 }
3296
3297 let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
3298 let receipt_result =
3299 match tokio::time::timeout(remaining, self.verification.verify_receipt(&tx_hash))
3300 .await
3301 {
3302 Ok(result) => result,
3303 Err(_) => break,
3304 };
3305
3306 match receipt_result {
3307 VerificationOutcome::Verified(verified_receipt) => {
3308 let receipt = &verified_receipt.value;
3309 let canonical_verification = required_verification(
3310 self.verification.verify_block(receipt.block_number).await,
3311 "receipt inclusion header",
3312 )?;
3313 let canonical = canonical_verification.value;
3314
3315 if canonical.hash != receipt.block_hash {
3316 continue;
3317 }
3318
3319 if let Some(mut finality) = self.receipt_is_stably_finalized(receipt).await? {
3320 finality.decisions.insert(
3321 0,
3322 verification_decision(
3323 &canonical_verification,
3324 Some(receipt.block_number),
3325 Some(receipt.block_number),
3326 ),
3327 );
3328 finality.decisions.insert(
3329 0,
3330 verification_decision(
3331 &verified_receipt,
3332 Some(receipt.block_number),
3333 Some(receipt.block_number),
3334 ),
3335 );
3336 let included = IncludedTransaction {
3337 intent_id: prepared.intent_id,
3338 nonce: prepared.nonce,
3339 tx_hash,
3340 block_number: receipt.block_number,
3341 receipt: receipt.clone(),
3342 finality,
3343 };
3344 return if receipt.status {
3345 Ok(InclusionOutcome::Finalized(included))
3346 } else {
3347 Ok(InclusionOutcome::Reverted(included))
3348 };
3349 }
3350 }
3351 VerificationOutcome::Retryable(_) => {
3352 continue;
3353 }
3354 VerificationOutcome::Disagreement(_) => {
3355 return Ok(InclusionOutcome::Pending(format!(
3356 "Receipt verification disagreed for transaction {tx_hash}; the intent stays occupied for reconciliation"
3357 )));
3358 }
3359 VerificationOutcome::Unavailable(_) => {
3360 log::warn!(
3361 "Finality poll {}/{} for transaction {tx_hash} was unavailable",
3362 attempt + 1,
3363 self.receipt_max_polls
3364 );
3365 }
3366 VerificationOutcome::LocallyInvalid(_) => {
3367 anyhow::bail!(
3368 "Receipt verification is locally invalid for transaction {tx_hash}"
3369 );
3370 }
3371 }
3372 }
3373
3374 self.database
3375 .record_execution_status(
3376 prepared.intent_id,
3377 &tx_hash.to_string(),
3378 TransactionStatus::Dropped,
3379 None,
3380 None,
3381 None,
3382 None,
3383 None,
3384 )
3385 .await?;
3386 Ok(InclusionOutcome::Pending(format!(
3387 "Timed out awaiting finality of transaction {tx_hash}; the intent stays occupied for reconciliation"
3388 )))
3389 }
3390
3391 async fn receipt_is_stably_finalized(
3392 &self,
3393 receipt: &RpcTransactionReceipt,
3394 ) -> anyhow::Result<Option<StableFinality>> {
3395 let finalized_verification = match self.verification.verify_finalized_header().await {
3396 VerificationOutcome::Verified(verified) => verified,
3397 VerificationOutcome::Retryable(_) | VerificationOutcome::Unavailable(_) => {
3398 return Ok(None);
3399 }
3400 VerificationOutcome::Disagreement(_) => {
3401 anyhow::bail!("Finalized header verification disagreed")
3402 }
3403 VerificationOutcome::LocallyInvalid(_) => {
3404 anyhow::bail!("Finalized header verification is locally invalid")
3405 }
3406 };
3407 let finalized = finalized_verification.value;
3408 if finalized.number < receipt.block_number {
3409 return Ok(None);
3410 }
3411
3412 let checkpoint_verification = required_verification(
3413 self.verification.verify_checkpoint().await,
3414 "finality checkpoint reread",
3415 )?;
3416 let checkpoint = checkpoint_verification.value;
3417 let mut decisions = vec![verification_decision(
3418 &checkpoint_verification,
3419 Some(checkpoint.number),
3420 Some(checkpoint.number),
3421 )];
3422 let position = self
3423 .database
3424 .load_execution_verification_position(
3425 self.chain_id,
3426 &self.wallet_address.to_string(),
3427 &self.manifest_version,
3428 &self.manifest_digest,
3429 )
3430 .await?
3431 .ok_or_else(|| anyhow::anyhow!("Execution verification ledger is not initialized"))?;
3432 let durable_tip = parse_verified_header(&position.finalized_tip)?;
3433 anyhow::ensure!(
3434 durable_tip.number >= checkpoint.number,
3435 "Durable finalized header tip precedes the trusted checkpoint"
3436 );
3437 let mut finalized_headers = vec![durable_tip];
3438 let durable_tip_verification = required_verification(
3439 self.verification.verify_block(durable_tip.number).await,
3440 "finality durable header tip",
3441 )?;
3442 anyhow::ensure!(
3443 durable_tip_verification.value == durable_tip,
3444 "Durable finalized header tip conflicts with independent sources"
3445 );
3446 decisions.push(verification_decision(
3447 &durable_tip_verification,
3448 Some(durable_tip.number),
3449 Some(durable_tip.number),
3450 ));
3451 anyhow::ensure!(
3452 finalized.number >= durable_tip.number,
3453 "Verified finalized height regressed below the durable finalized header tip"
3454 );
3455 let mut ancestry_cursor = durable_tip;
3456 while ancestry_cursor.number < finalized.number {
3457 let end = ancestry_cursor
3458 .number
3459 .saturating_add(4_096)
3460 .min(finalized.number);
3461 let start = ancestry_cursor.number.saturating_add(1);
3462 let ancestry_verification = required_verification(
3463 self.verification
3464 .verify_header_window(ancestry_cursor, end)
3465 .await,
3466 "finality ancestry",
3467 )?;
3468 let ancestry = &ancestry_verification.value;
3469 ancestry_cursor = *ancestry
3470 .last()
3471 .expect("nonempty finality ancestry advances the cursor");
3472 decisions.push(verification_decision(
3473 &ancestry_verification,
3474 Some(start),
3475 Some(end),
3476 ));
3477 finalized_headers.extend(ancestry.iter().copied());
3478 }
3479 anyhow::ensure!(
3480 ancestry_cursor == finalized,
3481 "Finalized header conflicts with its verified ancestry"
3482 );
3483
3484 let canonical_again_verification = required_verification(
3485 self.verification.verify_block(receipt.block_number).await,
3486 "finality inclusion header reread",
3487 )?;
3488 let canonical_again = canonical_again_verification.value;
3489 let finalized_again_verification = required_verification(
3490 self.verification.verify_block(finalized.number).await,
3491 "finalized header reread",
3492 )?;
3493 let finalized_again = finalized_again_verification.value;
3494 anyhow::ensure!(
3495 canonical_again.hash == receipt.block_hash && finalized_again == finalized,
3496 "Finality verification disagreed with the receipt or finalized header"
3497 );
3498 decisions.extend([
3499 verification_decision(
3500 &finalized_verification,
3501 Some(finalized.number),
3502 Some(finalized.number),
3503 ),
3504 verification_decision(
3505 &canonical_again_verification,
3506 Some(receipt.block_number),
3507 Some(receipt.block_number),
3508 ),
3509 verification_decision(
3510 &finalized_again_verification,
3511 Some(finalized.number),
3512 Some(finalized.number),
3513 ),
3514 ]);
3515 Ok(Some(StableFinality {
3516 decisions,
3517 inclusion_header: durable_verified_header(&canonical_again),
3518 finalized_headers: finalized_headers
3519 .iter()
3520 .map(durable_verified_header)
3521 .collect(),
3522 }))
3523 }
3524
3525 async fn commit_verified_finality(
3526 &self,
3527 included: &IncludedTransaction,
3528 status: TransactionStatus,
3529 verified_postconditions: &[ExecutionVerificationDecision],
3530 ) -> anyhow::Result<()> {
3531 anyhow::ensure!(
3532 matches!(
3533 status,
3534 TransactionStatus::Finalized | TransactionStatus::Reverted
3535 ) && included.receipt.status == (status == TransactionStatus::Finalized),
3536 "Finality status conflicts with the verified transaction receipt"
3537 );
3538 let mut decisions = included.finality.decisions.clone();
3539 decisions.extend_from_slice(verified_postconditions);
3540 let wallet_address = self.wallet_address.to_string();
3541 let transaction_hash = included.tx_hash.to_string();
3542 let block_hash = included.receipt.block_hash.to_string();
3543 let effective_gas_price = included.receipt.effective_gas_price.to_string();
3544 self.database
3545 .record_execution_finality_verified(&ExecutionFinalityTransition {
3546 intent_id: included.intent_id,
3547 chain_id: self.chain_id,
3548 wallet_address: &wallet_address,
3549 nonce: included.nonce,
3550 transaction_hash: &transaction_hash,
3551 status,
3552 block_number: included.block_number,
3553 block_hash: &block_hash,
3554 receipt_success: included.receipt.status,
3555 gas_used: included.receipt.gas_used,
3556 effective_gas_price: &effective_gas_price,
3557 manifest_version: &self.manifest_version,
3558 manifest_digest: &self.manifest_digest,
3559 provider_ids: &self.provider_ids,
3560 operator_ids: &self.operator_ids,
3561 failure_domain_ids: &self.failure_domain_ids,
3562 decisions: &decisions,
3563 finalized_headers: &included.finality.finalized_headers,
3564 })
3565 .await
3566 }
3567
3568 fn release_slot(&self) {
3569 *self.in_flight.lock() = None;
3570 }
3571}
3572
3573fn replacement_scan_range(from_block: u64, head_block: u64) -> anyhow::Result<RangeInclusive<u64>> {
3574 anyhow::ensure!(
3575 head_block >= from_block,
3576 "Canonical head {head_block} is behind execution creation block {from_block}"
3577 );
3578 let max_end = from_block.saturating_add(MAX_REPLACEMENT_SCAN_BLOCKS - 1);
3579 Ok(from_block..=head_block.min(max_end))
3580}
3581
3582fn current_unix_secs() -> anyhow::Result<u64> {
3583 SystemTime::now()
3584 .duration_since(UNIX_EPOCH)
3585 .map_err(|_| anyhow::anyhow!("Trusted host clock precedes the Unix epoch"))
3586 .map(|duration| duration.as_secs())
3587}
3588
3589fn validate_payload_operation_batch_size(batch_size: usize) -> anyhow::Result<i64> {
3590 anyhow::ensure!(
3591 (1..=MAX_PAYLOAD_OPERATION_BATCH_SIZE).contains(&batch_size),
3592 "Payload operation batch size must be between 1 and {MAX_PAYLOAD_OPERATION_BATCH_SIZE}"
3593 );
3594 Ok(i64::try_from(batch_size).expect("bounded payload batch size fits i64"))
3595}
3596
3597fn current_execution_hash(
3598 intent_id: i64,
3599 hashes: &[ExecutionTransactionHashRow],
3600) -> anyhow::Result<&ExecutionTransactionHashRow> {
3601 let mut current = hashes.iter().filter(|row| row.current);
3602 let row = current
3603 .next()
3604 .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} has no current hash"))?;
3605 anyhow::ensure!(
3606 current.next().is_none(),
3607 "Execution intent {intent_id} has more than one current hash"
3608 );
3609 Ok(row)
3610}
3611
3612fn open_execution_payload(
3613 keys: &PayloadKeySet,
3614 policy: PayloadPolicy,
3615 intent: &ExecutionIntentRow,
3616 hash: &ExecutionTransactionHashRow,
3617 reason: &str,
3618) -> anyhow::Result<Vec<u8>> {
3619 anyhow::ensure!(
3620 hash.payload_expected,
3621 "Execution transaction {} has no signed payload",
3622 hash.transaction_hash
3623 );
3624 anyhow::ensure!(
3625 hash.raw_transaction.is_none(),
3626 "Protected execution transaction {} contains plaintext",
3627 hash.transaction_hash
3628 );
3629 let envelope = hash.sealed_transaction.as_deref().ok_or_else(|| {
3630 anyhow::anyhow!(
3631 "Protected execution transaction {} has no sealed payload",
3632 hash.transaction_hash
3633 )
3634 })?;
3635 let context = payload_context(intent, hash, keys.deployment_id())?;
3636 let raw_transaction = keys.unseal(envelope, &context)?;
3637 log::info!(
3638 "Unsealed execution payload for intent {} transaction {} during {reason}",
3639 intent.id,
3640 hash.transaction_hash
3641 );
3642 authenticate_retained_payload(&raw_transaction, intent, hash, keys.deployment_id())?;
3643 if retained_payload_requires_policy(intent, hash, policy)? {
3644 authenticate_payload(&raw_transaction, intent, hash, policy, keys.deployment_id())
3645 .with_context(|| {
3646 format!(
3647 "execution intent {} transaction {} violates current execution policy",
3648 intent.id, hash.transaction_hash
3649 )
3650 })?;
3651 }
3652 Ok(raw_transaction)
3653}
3654
3655fn receipt_max_polls(receipt_timeout_secs: u64) -> u32 {
3656 u32::try_from(receipt_timeout_secs.max(1)).unwrap_or(u32::MAX)
3657}
3658
3659fn receipt_timeout(receipt_timeout_secs: u64) -> Duration {
3660 Duration::from_secs(receipt_timeout_secs.clamp(1, u64::from(u32::MAX)))
3661}
3662
3663#[derive(Debug)]
3665struct SwapPlan {
3666 order: OrderAny,
3667 pool: Pool,
3668 quote_currency: Currency,
3669 instrument_id: InstrumentId,
3670 pool_address: Address,
3671 router: Address,
3672 factory: Address,
3673 weth: Address,
3674 token_in: Address,
3675 token_out: Address,
3676 fee: U24,
3677 amount_in: U256,
3678 min_amount_out: U256,
3679 slippage_bps: u32,
3680 quote_spend_ceiling: Option<QuoteSpendCeiling>,
3681 profiler_position: Option<BlockPosition>,
3682}
3683
3684#[derive(Debug, Clone)]
3685struct SwapQuoteAnchors {
3686 watermark: VerifiedBlockHeader,
3687 state: VerifiedBlockHeader,
3688 quote_contract: Address,
3689 token_in: Address,
3690 token_out: Address,
3691 fee: U24,
3692 quote_kind: SwapQuoteKind,
3693 quote: UniswapV3Quote,
3694 precondition_decisions: Vec<ExecutionVerificationDecision>,
3695}
3696
3697#[derive(Debug, Clone, Copy)]
3698enum SwapQuoteKind {
3699 ExactInput(U256),
3700 ExactOutput(U256),
3701}
3702
3703async fn execute_swap(
3713 mut plan: SwapPlan,
3714 executor: TransactionExecutor,
3715 emitter: ExecutionEventEmitter,
3716 max_quote_age_blocks: u64,
3717 deadline_seconds: u64,
3718) -> anyhow::Result<()> {
3719 let order = &plan.order;
3720
3721 if let Err(e) = executor.claim_slot(TransactionPurpose::Swap) {
3722 emitter.emit_order_denied(order, &e.to_string());
3723 return Ok(());
3724 }
3725
3726 let Some(profiler_position) = plan.profiler_position.as_ref() else {
3727 release_preparing_slot(&executor.in_flight);
3728 emitter.emit_order_denied(order, "Pool profiler has no quote provenance");
3729 return Ok(());
3730 };
3731 let mut swap_anchors = match validate_swap_quote(
3732 profiler_position,
3733 &plan,
3734 max_quote_age_blocks,
3735 &executor.verification,
3736 &executor.deployment_manifest,
3737 )
3738 .await
3739 {
3740 Ok(anchors) => anchors,
3741 Err(e) => {
3742 release_preparing_slot(&executor.in_flight);
3743 emitter.emit_order_denied(order, &e.to_string());
3744 return Ok(());
3745 }
3746 };
3747 let (amount_in, min_amount_out) = match verified_swap_amounts(&plan, swap_anchors.quote) {
3748 Ok(amounts) => amounts,
3749 Err(e) => {
3750 release_preparing_slot(&executor.in_flight);
3751 emitter.emit_order_denied(order, &e.to_string());
3752 return Ok(());
3753 }
3754 };
3755 plan.amount_in = amount_in;
3756 plan.min_amount_out = min_amount_out;
3757 let deadline = match swap_anchors.state.timestamp.checked_add(deadline_seconds) {
3758 Some(deadline) => deadline,
3759 None => {
3760 release_preparing_slot(&executor.in_flight);
3761 emitter.emit_order_denied(
3762 order,
3763 &format!(
3764 "Swap deadline overflow: anchor timestamp {} plus `deadline_seconds` {deadline_seconds} exceeds u64",
3765 swap_anchors.state.timestamp
3766 ),
3767 );
3768 return Ok(());
3769 }
3770 };
3771
3772 swap_anchors.precondition_decisions =
3773 match check_swap_preconditions(&plan, swap_anchors.state.number, &executor).await {
3774 Ok(decisions) => decisions,
3775 Err(e) => {
3776 release_preparing_slot(&executor.in_flight);
3777 emitter.emit_order_denied(order, &e.to_string());
3778 return Ok(());
3779 }
3780 };
3781
3782 let calldata = UniswapV3SwapRouter::exactInputSingleCall {
3783 params: UniswapV3SwapRouter::ExactInputSingleParams {
3784 tokenIn: plan.token_in,
3785 tokenOut: plan.token_out,
3786 fee: plan.fee,
3787 recipient: executor.wallet_address,
3788 deadline: U256::from(deadline),
3789 amountIn: plan.amount_in,
3790 amountOutMinimum: plan.min_amount_out,
3791 sqrtPriceLimitX96: U160::ZERO,
3792 },
3793 }
3794 .abi_encode();
3795
3796 let calldata = Bytes::from(calldata);
3797 let intent = ExecutionIntentInsert {
3798 chain_id: executor.chain_id,
3799 wallet_address: executor.wallet_address.to_string(),
3800 purpose: TransactionPurpose::Swap.as_str().to_string(),
3801 client_order_id: Some(order.client_order_id().to_string()),
3802 trader_id: Some(order.trader_id().to_string()),
3803 strategy_id: Some(order.strategy_id().to_string()),
3804 account_id: Some(executor.account_id.to_string()),
3805 instrument_id: Some(plan.instrument_id.to_string()),
3806 pool_address: Some(plan.pool_address.to_string()),
3807 transaction_to: plan.router.to_string(),
3808 transaction_input: hex::encode_prefixed(&calldata),
3809 transaction_value: U256::ZERO.to_string(),
3810 amount_in: Some(plan.amount_in.to_string()),
3811 created_block: swap_anchors.state.number,
3812 };
3813 let intent = match executor.database.reserve_execution_intent(&intent).await {
3814 Ok(intent) => intent,
3815 Err(e) => {
3816 release_preparing_if_reservation_not_committed(&executor.in_flight, &e);
3817 emitter.emit_order_denied(order, &e.to_string());
3818 return Ok(());
3819 }
3820 };
3821 let prepared = match executor
3822 .prepare_and_sign_swap(
3823 intent.id,
3824 intent.created_block,
3825 plan.router,
3826 U256::ZERO,
3827 calldata,
3828 &swap_anchors,
3829 )
3830 .await
3831 {
3832 Ok(prepared) => prepared,
3833 Err(e) => {
3834 if executor
3835 .database
3836 .mark_execution_intent_recoverable(intent.id)
3837 .await
3838 .is_ok()
3839 {
3840 release_preparing_slot(&executor.in_flight);
3841 }
3842 emitter.emit_order_denied(order, &e.to_string());
3843 return Ok(());
3844 }
3845 };
3846
3847 if let Err(e) = executor
3848 .fill_and_persist(&prepared, TransactionPurpose::Swap)
3849 .await
3850 {
3851 emitter.emit_order_denied(order, &e.to_string());
3852 return Ok(());
3853 }
3854
3855 let broadcast = executor.broadcast(&prepared).await?;
3856 emitter.emit_order_submitted(order);
3857 executor
3858 .database
3859 .mark_execution_event_emitted(intent.id, "acknowledgement")
3860 .await?;
3861
3862 if let BroadcastOutcome::Ambiguous(message) = broadcast {
3863 log::warn!("{message}");
3864 }
3865
3866 let trace_purpose = match plan.order.order_side() {
3867 OrderSide::Sell => "swap_sell",
3868 OrderSide::Buy => "swap_buy",
3869 };
3870
3871 match executor.await_finality(&prepared).await? {
3872 InclusionOutcome::Finalized(mut included) => {
3873 included.finality.decisions.extend(
3874 verify_finalized_transaction(
3875 &included,
3876 &intent,
3877 prepared.nonce,
3878 &prepared.raw_tx,
3879 &executor,
3880 trace_purpose,
3881 )
3882 .await?,
3883 );
3884 let fill = validate_finalized_swap_fill(&plan, &included)?;
3885 let wallet = load_verified_wallet_after_fill(&plan, &included, &executor).await?;
3886 executor
3887 .commit_verified_finality(
3888 &included,
3889 TransactionStatus::Finalized,
3890 &wallet.decisions,
3891 )
3892 .await?;
3893 complete_finalized_swap(
3894 &plan,
3895 intent.id,
3896 included.tx_hash,
3897 fill,
3898 wallet,
3899 &executor,
3900 &emitter,
3901 )
3902 .await?;
3903 executor.release_slot();
3904 Ok(())
3905 }
3906 InclusionOutcome::Reverted(mut included) => {
3907 included.finality.decisions.extend(
3908 verify_finalized_transaction(
3909 &included,
3910 &intent,
3911 prepared.nonce,
3912 &prepared.raw_tx,
3913 &executor,
3914 trace_purpose,
3915 )
3916 .await?,
3917 );
3918 executor
3919 .commit_verified_finality(&included, TransactionStatus::Reverted, &[])
3920 .await?;
3921 send_reverted_order(&emitter, order, &included)?;
3922 executor
3923 .database
3924 .mark_execution_event_emitted(intent.id, "terminal")
3925 .await?;
3926 executor.release_slot();
3927 Ok(())
3928 }
3929 InclusionOutcome::Pending(message) => anyhow::bail!(message),
3930 }
3931}
3932
3933async fn validate_swap_quote(
3934 position: &BlockPosition,
3935 plan: &SwapPlan,
3936 max_age_blocks: u64,
3937 verification: &VerificationCoordinator,
3938 manifest: &BlockchainDeploymentManifest,
3939) -> anyhow::Result<SwapQuoteAnchors> {
3940 let block_hash = position.block_hash.as_deref().ok_or_else(|| {
3941 anyhow::anyhow!(
3942 "Pool state at block {} has no ingestion-time block hash; refresh the profiler before execution",
3943 position.number
3944 )
3945 })?;
3946 let expected_block_hash = B256::from_str(block_hash)
3947 .with_context(|| format!("Invalid profiler block hash {block_hash}"))?;
3948 let now_unix_secs = current_unix_secs()?;
3949 let head = verified_value(
3950 verification.verify_decision_header(now_unix_secs).await,
3951 "swap decision header",
3952 )?;
3953 validate_quote_age(position.number, head.number, max_age_blocks)?;
3954 let canonical_block = verified_value(
3955 verification.verify_block(position.number).await,
3956 "profiler watermark header",
3957 )?;
3958 anyhow::ensure!(
3959 canonical_block.hash == expected_block_hash,
3960 "Pool state block {} changed from {} to {}; refresh the profiler before execution",
3961 position.number,
3962 expected_block_hash,
3963 canonical_block.hash
3964 );
3965
3966 let snapshot_transaction = position.transaction_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
3967 let snapshot_log = position.log_index == BLOCK_SCOPED_SNAPSHOT_INDEX;
3968 anyhow::ensure!(
3969 snapshot_transaction == snapshot_log,
3970 "Pool state at block {} has an invalid partial snapshot watermark",
3971 position.number
3972 );
3973
3974 if snapshot_transaction {
3975 let snapshot_hash = B256::from_str(&position.transaction_hash)
3976 .with_context(|| "Invalid block-scoped snapshot hash")?;
3977 anyhow::ensure!(
3978 snapshot_hash == expected_block_hash,
3979 "Block-scoped snapshot hash {snapshot_hash} does not match ingestion hash {expected_block_hash}"
3980 );
3981 } else {
3982 validate_profiler_event_verified(
3983 position,
3984 expected_block_hash,
3985 plan.pool_address,
3986 &plan.pool,
3987 verification,
3988 )
3989 .await?;
3990 }
3991
3992 let ancestry = verified_value(
3993 verification
3994 .verify_header_window(canonical_block, head.number)
3995 .await,
3996 "profiler-to-decision ancestry",
3997 )?;
3998
3999 if let Some(last) = ancestry.last() {
4000 anyhow::ensure!(
4001 *last == head,
4002 "Swap decision header conflicts with profiler ancestry"
4003 );
4004 } else {
4005 anyhow::ensure!(
4006 canonical_block == head,
4007 "Empty profiler ancestry does not end at the decision header"
4008 );
4009 }
4010 verified_value(
4011 verification
4012 .verify_deployment_manifest(manifest, head.number)
4013 .await,
4014 "swap deployment manifest",
4015 )?;
4016 let quote_contract = validate_manifest_pool(plan, manifest)?;
4017 let quote_kind = match plan.order.order_side() {
4018 OrderSide::Sell => SwapQuoteKind::ExactInput(quantity_to_raw_amount(
4019 plan.order.quantity(),
4020 plan.pool.get_base_token().decimals,
4021 )?),
4022 OrderSide::Buy => SwapQuoteKind::ExactOutput(quantity_to_raw_amount(
4023 plan.order.quantity(),
4024 plan.pool.get_base_token().decimals,
4025 )?),
4026 };
4027 let quote = verified_value(
4028 verify_swap_quote(verification, quote_contract, plan, quote_kind, head.number).await,
4029 "independent swap quote",
4030 )?;
4031
4032 Ok(SwapQuoteAnchors {
4033 watermark: canonical_block,
4034 state: head,
4035 quote_contract,
4036 token_in: plan.token_in,
4037 token_out: plan.token_out,
4038 fee: plan.fee,
4039 quote_kind,
4040 quote,
4041 precondition_decisions: Vec::new(),
4042 })
4043}
4044
4045async fn verify_swap_quote(
4046 verification: &VerificationCoordinator,
4047 quote_contract: Address,
4048 plan: &SwapPlan,
4049 kind: SwapQuoteKind,
4050 block: u64,
4051) -> VerificationOutcome<UniswapV3Quote> {
4052 match kind {
4053 SwapQuoteKind::ExactInput(amount_in) => {
4054 verification
4055 .verify_quote_exact_input_single(
4056 "e_contract,
4057 plan.token_in,
4058 plan.token_out,
4059 amount_in,
4060 plan.fee,
4061 block,
4062 )
4063 .await
4064 }
4065 SwapQuoteKind::ExactOutput(amount_out) => {
4066 verification
4067 .verify_quote_exact_output_single(
4068 "e_contract,
4069 plan.token_in,
4070 plan.token_out,
4071 amount_out,
4072 plan.fee,
4073 block,
4074 )
4075 .await
4076 }
4077 }
4078}
4079
4080fn verified_swap_amounts(plan: &SwapPlan, quote: UniswapV3Quote) -> anyhow::Result<(U256, U256)> {
4081 anyhow::ensure!(
4082 !quote.amount.is_zero(),
4083 "Independent swap quote returned zero"
4084 );
4085 let base_amount =
4086 quantity_to_raw_amount(plan.order.quantity(), plan.pool.get_base_token().decimals)?;
4087 let slippage_bps = plan.slippage_bps;
4088 match plan.order.order_side() {
4089 OrderSide::Sell => Ok((
4090 base_amount,
4091 derive_min_amount_out(quote.amount, slippage_bps)?,
4092 )),
4093 OrderSide::Buy => {
4094 let ceiling = plan.quote_spend_ceiling.ok_or_else(|| {
4095 anyhow::anyhow!(
4096 "No quote spend ceiling for BUY token pair {} -> {}",
4097 plan.token_in,
4098 plan.token_out
4099 )
4100 })?;
4101 anyhow::ensure!(
4102 quote.amount <= ceiling.max_amount,
4103 "BUY quote amount {} exceeds the configured quote-spend maximum {} for {} -> {}",
4104 quote.amount,
4105 ceiling.max_amount,
4106 plan.token_in,
4107 plan.token_out
4108 );
4109 Ok((
4110 quote.amount,
4111 derive_min_amount_out(base_amount, slippage_bps)?,
4112 ))
4113 }
4114 }
4115}
4116
4117fn validate_manifest_pool(
4118 plan: &SwapPlan,
4119 manifest: &BlockchainDeploymentManifest,
4120) -> anyhow::Result<Address> {
4121 let matching = manifest
4122 .pools
4123 .iter()
4124 .filter(|pool| Address::from_str(&pool.address).ok() == Some(plan.pool_address))
4125 .collect::<Vec<_>>();
4126 anyhow::ensure!(
4127 matching.len() == 1,
4128 "Pool {} does not have exactly one deployment manifest definition",
4129 plan.pool_address
4130 );
4131 let pool = matching[0];
4132 let token0 = Address::from_str(&pool.token0)?;
4133 let token1 = Address::from_str(&pool.token1)?;
4134 let factory = Address::from_str(&pool.factory)?;
4135 let quote_contract = Address::from_str(&pool.quote_contract)?;
4136 anyhow::ensure!(
4137 token0 == plan.pool.token0.address
4138 && token1 == plan.pool.token1.address
4139 && pool.fee == plan.pool.fee.expect("validated pool fee")
4140 && factory == plan.factory,
4141 "Cached pool {} does not match its deployment manifest identity",
4142 plan.pool_address
4143 );
4144
4145 for token in [&plan.pool.token0, &plan.pool.token1] {
4146 let identities = manifest
4147 .tokens
4148 .iter()
4149 .filter(|identity| Address::from_str(&identity.address).ok() == Some(token.address))
4150 .collect::<Vec<_>>();
4151 anyhow::ensure!(
4152 identities.len() == 1,
4153 "Token {} does not have exactly one deployment manifest identity",
4154 token.address
4155 );
4156 let identity = identities[0];
4157 anyhow::ensure!(
4158 identity.name == token.name
4159 && identity.symbol == token.symbol
4160 && identity.decimals == token.decimals,
4161 "Cached token {} does not match its deployment manifest identity",
4162 token.address
4163 );
4164 let expected_role = if token.address == plan.pool.get_base_token().address {
4165 "base"
4166 } else {
4167 "quote"
4168 };
4169 anyhow::ensure!(
4170 matches!(identity.asset_role.as_str(), "both") || identity.asset_role == expected_role,
4171 "Token {} is not permitted as the pool {expected_role} asset",
4172 token.address
4173 );
4174 }
4175 Ok(quote_contract)
4176}
4177
4178async fn validate_profiler_event_verified(
4179 position: &BlockPosition,
4180 expected_block_hash: B256,
4181 pool_address: Address,
4182 pool: &Pool,
4183 verification: &VerificationCoordinator,
4184) -> anyhow::Result<()> {
4185 let transaction_hash = B256::from_str(&position.transaction_hash).with_context(|| {
4186 format!(
4187 "Invalid profiler transaction hash {}",
4188 position.transaction_hash
4189 )
4190 })?;
4191 let receipt = verified_value(
4192 verification.verify_receipt(&transaction_hash).await,
4193 "profiler watermark receipt",
4194 )?;
4195 anyhow::ensure!(
4196 receipt.status,
4197 "Profiler transaction did not execute successfully"
4198 );
4199 anyhow::ensure!(
4200 receipt.transaction_hash == transaction_hash,
4201 "Profiler receipt transaction hash does not match its ingestion watermark"
4202 );
4203 anyhow::ensure!(
4204 receipt.block_number == position.number
4205 && receipt.block_hash == expected_block_hash
4206 && receipt.transaction_index == u64::from(position.transaction_index),
4207 "Profiler receipt position does not match its ingestion watermark"
4208 );
4209 let matching_logs = receipt
4210 .logs
4211 .iter()
4212 .filter(|log| rpc_log::extract_log_index(log).ok() == Some(position.log_index))
4213 .collect::<Vec<_>>();
4214 anyhow::ensure!(
4215 matching_logs.len() == 1,
4216 "Profiler receipt contains {} logs at global index {}; expected exactly one",
4217 matching_logs.len(),
4218 position.log_index
4219 );
4220 let log = matching_logs[0];
4221 let log_transaction_hash = B256::from_str(&rpc_log::extract_transaction_hash(log)?)
4222 .with_context(|| "Invalid profiler log transaction hash")?;
4223 let log_block_hash = log
4224 .block_hash
4225 .as_deref()
4226 .ok_or_else(|| anyhow::anyhow!("Profiler log has no block hash"))?;
4227 anyhow::ensure!(
4228 !log.removed
4229 && log_transaction_hash == transaction_hash
4230 && rpc_log::extract_block_number(log)? == position.number
4231 && rpc_log::extract_transaction_index(log)? == position.transaction_index
4232 && B256::from_str(log_block_hash)? == expected_block_hash,
4233 "Profiler log position does not match its ingestion watermark"
4234 );
4235 anyhow::ensure!(
4236 rpc_log::extract_address(log)? == pool_address,
4237 "Profiler watermark log did not come from expected pool {pool_address}"
4238 );
4239 let signature = log
4240 .topics
4241 .first()
4242 .ok_or_else(|| anyhow::anyhow!("Profiler watermark log has no event signature"))?;
4243 let supported =
4244 profiler_event_signatures(pool).any(|expected| expected.eq_ignore_ascii_case(signature));
4245 anyhow::ensure!(
4246 supported,
4247 "Profiler watermark log has an unsupported event signature"
4248 );
4249 Ok(())
4250}
4251
4252fn profiler_event_signatures(pool: &Pool) -> impl Iterator<Item = &str> {
4253 [
4254 Some(pool.dex.swap_created_event.as_ref()),
4255 Some(pool.dex.mint_created_event.as_ref()),
4256 Some(pool.dex.burn_created_event.as_ref()),
4257 Some(pool.dex.collect_created_event.as_ref()),
4258 pool.dex.flash_created_event.as_deref(),
4259 pool.dex.fee_protocol_update_event.as_deref(),
4260 pool.dex.fee_protocol_collect_event.as_deref(),
4261 ]
4262 .into_iter()
4263 .flatten()
4264}
4265
4266fn validate_quote_age(
4267 profiler_block: u64,
4268 latest_block: u64,
4269 max_age_blocks: u64,
4270) -> anyhow::Result<()> {
4271 anyhow::ensure!(
4272 profiler_block <= latest_block,
4273 "Pool state at block {profiler_block} is ahead of the latest block {latest_block}; the execution RPC endpoint lags the data feed"
4274 );
4275 let quote_age = latest_block - profiler_block;
4276 anyhow::ensure!(
4277 quote_age <= max_age_blocks,
4278 "Stale quote: pool state at block {profiler_block}, latest block {latest_block}, exceeds `max_quote_age_blocks` {max_age_blocks}"
4279 );
4280 Ok(())
4281}
4282
4283fn validate_rpc_transaction_matches_payload(
4284 transaction: &RpcTransaction,
4285 raw_transaction: &[u8],
4286) -> anyhow::Result<()> {
4287 let signed = decode_signed_transaction(raw_transaction)?;
4288 anyhow::ensure!(
4289 transaction.hash == signed.hash
4290 && transaction.from == signed.signer
4291 && transaction.nonce == signed.nonce
4292 && transaction.chain_id == Some(signed.chain_id)
4293 && transaction.transaction_type == Some(2)
4294 && transaction.to == Some(signed.to)
4295 && transaction.input == signed.input
4296 && transaction.value == signed.value
4297 && transaction.gas == Some(signed.gas_limit)
4298 && transaction.max_fee_per_gas == Some(U256::from(signed.max_fee_per_gas))
4299 && transaction.max_priority_fee_per_gas
4300 == Some(U256::from(signed.max_priority_fee_per_gas)),
4301 "Verified transaction fields differ from the authenticated signed payload"
4302 );
4303 Ok(())
4304}
4305
4306async fn verify_finalized_transaction(
4307 included: &IncludedTransaction,
4308 intent: &ExecutionIntentRow,
4309 nonce: u64,
4310 raw_transaction: &[u8],
4311 executor: &TransactionExecutor,
4312 trace_purpose: &str,
4313) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4314 verify_finalized_transaction_identity(
4315 included,
4316 intent,
4317 nonce,
4318 raw_transaction,
4319 &executor.verification,
4320 executor.wallet_address,
4321 executor.chain_id,
4322 &executor.deployment_manifest,
4323 trace_purpose,
4324 )
4325 .await
4326}
4327
4328#[expect(clippy::too_many_arguments)]
4329async fn verify_finalized_transaction_identity(
4330 included: &IncludedTransaction,
4331 intent: &ExecutionIntentRow,
4332 nonce: u64,
4333 raw_transaction: &[u8],
4334 verification: &VerificationCoordinator,
4335 wallet_address: Address,
4336 chain_id: u32,
4337 deployment_manifest: &BlockchainDeploymentManifest,
4338 trace_purpose: &str,
4339) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4340 let transaction_verification = required_verification(
4341 verification.verify_transaction(&included.tx_hash).await,
4342 "finalized transaction",
4343 )?;
4344 let transaction = &transaction_verification.value;
4345 let signed = decode_signed_transaction(raw_transaction)?;
4346 let (expected_to, expected_input, expected_value) = persisted_call_fields(intent)?;
4347 anyhow::ensure!(
4348 included.receipt.transaction_hash == included.tx_hash
4349 && signed.hash == included.tx_hash
4350 && signed.signer == wallet_address
4351 && signed.chain_id == u64::from(chain_id)
4352 && signed.nonce == nonce
4353 && signed.to == expected_to
4354 && signed.input == expected_input
4355 && signed.value == expected_value,
4356 "Finalized transaction does not match the authenticated signed payload and persisted intent"
4357 );
4358 validate_rpc_transaction_matches_payload(transaction, raw_transaction)
4359 .context("finalized transaction identity mismatch")?;
4360
4361 let trace_verification = required_verification(
4362 verification.verify_call_trace(&included.tx_hash).await,
4363 "finalized call trace",
4364 )?;
4365 validate_call_trace(
4366 &trace_verification.value,
4367 &signed,
4368 included.receipt.status,
4369 trace_purpose,
4370 deployment_manifest,
4371 )?;
4372 let deployment_verification = required_verification(
4373 verification
4374 .verify_deployment_manifest(deployment_manifest, included.block_number)
4375 .await,
4376 "inclusion deployment manifest",
4377 )?;
4378
4379 Ok(vec![
4380 verification_decision(
4381 &transaction_verification,
4382 Some(included.block_number),
4383 Some(included.block_number),
4384 ),
4385 verification_decision(
4386 &trace_verification,
4387 Some(included.block_number),
4388 Some(included.block_number),
4389 ),
4390 verification_decision(
4391 &deployment_verification,
4392 Some(included.block_number),
4393 Some(included.block_number),
4394 ),
4395 ])
4396}
4397
4398fn validate_call_trace(
4399 trace: &VerifiedCallTrace,
4400 signed: &crate::execution::transaction::DecodedSignedTransaction,
4401 receipt_success: bool,
4402 purpose: &str,
4403 manifest: &BlockchainDeploymentManifest,
4404) -> anyhow::Result<()> {
4405 anyhow::ensure!(
4406 trace.call_type == RpcCallType::Call
4407 && trace.from == signed.signer
4408 && trace.to == Some(signed.to)
4409 && trace.value == signed.value
4410 && trace.input_digest == keccak256(&signed.input)
4411 && trace.success == receipt_success,
4412 "Verified call-trace root differs from the authenticated transaction"
4413 );
4414 validate_internal_calls(&trace.calls, signed.to, purpose, manifest)
4415}
4416
4417fn validate_internal_calls(
4418 calls: &[VerifiedCallTrace],
4419 caller_context: Address,
4420 purpose: &str,
4421 manifest: &BlockchainDeploymentManifest,
4422) -> anyhow::Result<()> {
4423 for call in calls {
4424 anyhow::ensure!(
4425 call.from == caller_context,
4426 "Verified call trace child has an invalid caller context"
4427 );
4428 let target = call.to.ok_or_else(|| {
4429 anyhow::anyhow!("Verified call trace contains an operation without a target")
4430 })?;
4431 let call_type = match call.call_type {
4432 RpcCallType::Call => "call",
4433 RpcCallType::Callcode => "callcode",
4434 RpcCallType::Delegatecall => "delegatecall",
4435 RpcCallType::Staticcall => "staticcall",
4436 RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4437 anyhow::bail!("Verified call trace contains a forbidden state-changing operation")
4438 }
4439 };
4440 let permitted = manifest.call_edges.iter().any(|edge| {
4441 edge.purpose == purpose
4442 && edge.call_type.eq_ignore_ascii_case(call_type)
4443 && Address::from_str(&edge.caller).ok() == Some(call.from)
4444 && Address::from_str(&edge.target).ok() == Some(target)
4445 });
4446 anyhow::ensure!(
4447 permitted,
4448 "Verified call trace contains an unreviewed {call_type} edge {} -> {target} for {purpose}",
4449 call.from
4450 );
4451 let child_context = match call.call_type {
4452 RpcCallType::Call | RpcCallType::Staticcall => target,
4453 RpcCallType::Callcode | RpcCallType::Delegatecall => caller_context,
4454 RpcCallType::Create | RpcCallType::Create2 | RpcCallType::Selfdestruct => {
4455 unreachable!("forbidden operations return before child traversal")
4456 }
4457 };
4458 validate_internal_calls(&call.calls, child_context, purpose, manifest)?;
4459 }
4460 Ok(())
4461}
4462
4463async fn verify_wrap_balance_increase(
4464 executor: &TransactionExecutor,
4465 weth_address: &Address,
4466 amount_wei: U256,
4467 included: &IncludedTransaction,
4468) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4469 let previous_block = included.block_number.checked_sub(1).ok_or_else(|| {
4470 anyhow::anyhow!(
4471 "Included wrap transaction {} has invalid block number 0",
4472 included.tx_hash
4473 )
4474 })?;
4475 let call = ERC20::balanceOfCall {
4476 account: executor.wallet_address,
4477 }
4478 .abi_encode();
4479 let balance_before = required_verification(
4480 executor
4481 .verification
4482 .verify_decoded_call(
4483 None,
4484 weth_address,
4485 U256::ZERO,
4486 &call,
4487 previous_block,
4488 |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4489 )
4490 .await,
4491 "wrapped balance before finality",
4492 )
4493 .with_context(|| {
4494 format!(
4495 "failed to verify WETH balance before included transaction {} at block {previous_block}",
4496 included.tx_hash
4497 )
4498 })?;
4499 let balance_after = required_verification(
4500 executor
4501 .verification
4502 .verify_decoded_call(
4503 None,
4504 weth_address,
4505 U256::ZERO,
4506 &call,
4507 included.block_number,
4508 |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4509 )
4510 .await,
4511 "wrapped balance after finality",
4512 )
4513 .with_context(|| {
4514 format!(
4515 "failed to verify WETH balance after included transaction {} at block {}",
4516 included.tx_hash, included.block_number
4517 )
4518 })?;
4519 let expected_balance = balance_before
4520 .value
4521 .checked_add(amount_wei)
4522 .ok_or_else(|| {
4523 anyhow::anyhow!(
4524 "WETH balance overflow for included transaction {} at block {}",
4525 included.tx_hash,
4526 included.block_number
4527 )
4528 })?;
4529 anyhow::ensure!(
4530 balance_after.value == expected_balance,
4531 "WETH balance after transaction {} did not increase by {amount_wei}: expected {expected_balance}, was {}",
4532 included.tx_hash,
4533 balance_after.value
4534 );
4535
4536 Ok(vec![
4537 verification_decision(&balance_before, Some(previous_block), Some(previous_block)),
4538 verification_decision(
4539 &balance_after,
4540 Some(included.block_number),
4541 Some(included.block_number),
4542 ),
4543 ])
4544}
4545
4546async fn verify_approve_allowance(
4547 executor: &TransactionExecutor,
4548 token: &Address,
4549 router: &Address,
4550 amount: U256,
4551 included: &IncludedTransaction,
4552) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4553 let call = ERC20::allowanceCall {
4554 owner: executor.wallet_address,
4555 spender: *router,
4556 }
4557 .abi_encode();
4558 let allowance = required_verification(
4559 executor
4560 .verification
4561 .verify_decoded_call(
4562 None,
4563 token,
4564 U256::ZERO,
4565 &call,
4566 included.block_number,
4567 |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
4568 )
4569 .await,
4570 "router allowance after finality",
4571 )
4572 .with_context(|| {
4573 format!(
4574 "failed to verify router allowance after included transaction {} at block {}",
4575 included.tx_hash, included.block_number
4576 )
4577 })?;
4578 anyhow::ensure!(
4579 allowance.value == amount,
4580 "Router allowance after transaction {} does not equal the requested amount {amount}: was {}",
4581 included.tx_hash,
4582 allowance.value
4583 );
4584
4585 Ok(vec![verification_decision(
4586 &allowance,
4587 Some(included.block_number),
4588 Some(included.block_number),
4589 )])
4590}
4591
4592async fn complete_finalized_swap(
4593 plan: &SwapPlan,
4594 intent_id: i64,
4595 tx_hash: B256,
4596 fill: Option<FinalizedSwapFill>,
4597 wallet: VerifiedWalletRefresh,
4598 executor: &TransactionExecutor,
4599 emitter: &ExecutionEventEmitter,
4600) -> anyhow::Result<()> {
4601 if let Some(fill) = fill {
4602 let filled = OrderFilled::new(
4603 emitter.trader_id(),
4604 plan.order.strategy_id(),
4605 plan.order.instrument_id(),
4606 plan.order.client_order_id(),
4607 fill.venue_order_id,
4608 emitter.account_id(),
4609 fill.trade_id,
4610 plan.order.order_side(),
4611 plan.order.order_type(),
4612 fill.last_qty,
4613 fill.last_px,
4614 plan.quote_currency,
4615 LiquiditySide::Taker,
4616 execution_event_id(tx_hash, b"fill"),
4617 fill.ts_event,
4618 fill.ts_event,
4619 false,
4620 None,
4621 Some(fill.commission),
4622 None,
4623 );
4624 emitter.try_send_order_event(OrderEventAny::Filled(filled))?;
4625
4626 if fill.last_qty < plan.order.quantity() {
4627 let canceled = OrderCanceled::new(
4628 emitter.trader_id(),
4629 plan.order.strategy_id(),
4630 plan.order.instrument_id(),
4631 plan.order.client_order_id(),
4632 execution_event_id(tx_hash, b"partial_cancel"),
4633 fill.ts_event,
4634 fill.ts_event,
4635 false,
4636 Some(fill.venue_order_id),
4637 Some(emitter.account_id()),
4638 None,
4639 );
4640 emitter.try_send_order_event(OrderEventAny::Canceled(canceled))?;
4641 }
4642 }
4643
4644 *executor.wallet_balance.lock() = wallet.wallet_balance;
4645 emitter.try_emit_account_state(
4646 wallet.balances,
4647 vec![],
4648 true,
4649 get_atomic_clock_realtime().get_time_ns(),
4650 None,
4651 )?;
4652 executor
4653 .database
4654 .mark_execution_event_emitted(intent_id, "fill")
4655 .await
4656}
4657
4658fn send_reverted_order(
4659 emitter: &ExecutionEventEmitter,
4660 order: &OrderAny,
4661 included: &IncludedTransaction,
4662) -> anyhow::Result<()> {
4663 let ts_event = finalized_inclusion_time(included)?;
4664 let rejected = OrderRejected::new(
4665 emitter.trader_id(),
4666 order.strategy_id(),
4667 order.instrument_id(),
4668 order.client_order_id(),
4669 emitter.account_id(),
4670 format!("Transaction {} reverted on-chain", included.tx_hash).into(),
4671 execution_event_id(included.tx_hash, b"reverted"),
4672 ts_event,
4673 ts_event,
4674 false,
4675 false,
4676 );
4677 emitter.try_send_order_event(OrderEventAny::Rejected(rejected))
4678}
4679
4680fn finalized_inclusion_time(included: &IncludedTransaction) -> anyhow::Result<UnixNanos> {
4681 let inclusion = &included.finality.inclusion_header;
4682 anyhow::ensure!(
4683 inclusion.number == included.block_number
4684 && inclusion.hash == included.receipt.block_hash.to_string(),
4685 "Verified inclusion header does not match the finalized receipt"
4686 );
4687 let timestamp = inclusion.timestamp;
4688 let nanos = timestamp
4689 .checked_mul(NANOSECONDS_IN_SECOND)
4690 .ok_or_else(|| anyhow::anyhow!("Verified inclusion timestamp exceeds nanoseconds"))?;
4691 Ok(UnixNanos::from(nanos))
4692}
4693
4694fn execution_event_id(tx_hash: B256, event: &[u8]) -> UUID4 {
4695 let mut identity = Vec::with_capacity(tx_hash.len() + event.len());
4696 identity.extend_from_slice(tx_hash.as_slice());
4697 identity.extend_from_slice(event);
4698 let digest = keccak256(identity);
4699 let mut bytes = [0u8; 16];
4700 bytes.copy_from_slice(&digest[..16]);
4701 UUID4::from_bytes(bytes)
4702}
4703
4704struct FinalizedSwapFill {
4705 venue_order_id: VenueOrderId,
4706 trade_id: TradeId,
4707 last_qty: Quantity,
4708 last_px: Price,
4709 commission: Money,
4710 ts_event: UnixNanos,
4711}
4712
4713struct VerifiedWalletRefresh {
4714 wallet_balance: WalletBalance,
4715 balances: Vec<AccountBalance>,
4716 decisions: Vec<ExecutionVerificationDecision>,
4717}
4718
4719fn validate_finalized_swap_fill(
4720 plan: &SwapPlan,
4721 included: &IncludedTransaction,
4722) -> anyhow::Result<Option<FinalizedSwapFill>> {
4723 let signature =
4724 keccak256("Swap(address,address,int256,int256,uint160,uint128,int24)").to_string();
4725 let swap_logs = included
4726 .receipt
4727 .logs
4728 .iter()
4729 .filter(|log| {
4730 !log.removed
4731 && log.topics.first().is_some_and(|topic| topic == &signature)
4732 && Address::from_str(&log.address).ok() == Some(plan.pool_address)
4733 })
4734 .collect::<Vec<_>>();
4735 anyhow::ensure!(
4736 swap_logs.len() == 1,
4737 "Finalized transaction {} emitted {} Swap logs from expected pool {}; expected exactly one",
4738 included.tx_hash,
4739 swap_logs.len(),
4740 plan.pool_address
4741 );
4742 let log = swap_logs[0];
4743 let log_transaction_hash = B256::from_str(&rpc_log::extract_transaction_hash(log)?)
4744 .with_context(|| "Invalid finalized Swap log transaction hash")?;
4745 let log_block_hash = log
4746 .block_hash
4747 .as_deref()
4748 .ok_or_else(|| anyhow::anyhow!("Finalized Swap log has no block hash"))?;
4749 anyhow::ensure!(
4750 log_transaction_hash == included.tx_hash
4751 && rpc_log::extract_block_number(log)? == included.block_number
4752 && u64::from(rpc_log::extract_transaction_index(log)?)
4753 == included.receipt.transaction_index
4754 && B256::from_str(log_block_hash)
4755 .with_context(|| "Invalid finalized Swap log block hash")?
4756 == included.receipt.block_hash,
4757 "Finalized Swap log position does not match transaction {}",
4758 included.tx_hash
4759 );
4760
4761 let dex = crate::exchanges::get_dex_extended(plan.pool.chain.name, &plan.pool.dex.name)
4762 .ok_or_else(|| {
4763 anyhow::anyhow!(
4764 "No RPC Swap decoder for {}:{}",
4765 plan.pool.chain.name,
4766 plan.pool.dex.name
4767 )
4768 })?;
4769 let event = dex.parse_swap_event_rpc(log)?;
4770 let (base_amount, quote_amount) =
4771 if plan.pool.get_base_token().address == plan.pool.token0.address {
4772 (event.amount0, event.amount1)
4773 } else {
4774 (event.amount1, event.amount0)
4775 };
4776 let last_qty = match plan.order.order_side() {
4777 OrderSide::Sell => {
4778 anyhow::ensure!(
4779 base_amount.is_positive() && base_amount.unsigned_abs() == plan.amount_in,
4780 "Finalized Swap input {base_amount} does not match the persisted amount {}",
4781 plan.amount_in
4782 );
4783 plan.order.quantity()
4784 }
4785 OrderSide::Buy => {
4786 anyhow::ensure!(
4787 quote_amount.is_positive() && quote_amount.unsigned_abs() == plan.amount_in,
4788 "Finalized Swap input {quote_amount} does not match the persisted amount {}",
4789 plan.amount_in
4790 );
4791 anyhow::ensure!(
4792 base_amount.is_negative(),
4793 "Finalized Swap base amount {base_amount} is not a BUY output"
4794 );
4795 raw_amount_to_quantity(
4796 base_amount.unsigned_abs(),
4797 plan.pool.get_base_token().decimals,
4798 )?
4799 }
4800 };
4801
4802 let block = &included.finality.inclusion_header;
4803 anyhow::ensure!(
4804 block.number == included.block_number
4805 && block.hash == included.receipt.block_hash.to_string(),
4806 "Verified inclusion header {} does not match receipt hash {}",
4807 included.block_number,
4808 included.receipt.block_hash
4809 );
4810 let timestamp_ns = block
4811 .timestamp
4812 .checked_mul(NANOSECONDS_IN_SECOND)
4813 .ok_or_else(|| anyhow::anyhow!("Finalized block timestamp overflows nanoseconds"))?;
4814 let mut swap = event.to_pool_swap(
4815 Arc::clone(&plan.pool.chain),
4816 plan.instrument_id,
4817 plan.pool.pool_identifier,
4818 UnixNanos::from(timestamp_ns),
4819 );
4820 swap.calculate_trade_info(&plan.pool.token0, &plan.pool.token1, None)?;
4821 let trade = swap
4822 .trade_info
4823 .as_ref()
4824 .ok_or_else(|| anyhow::anyhow!("Finalized Swap has no calculated trade information"))?;
4825 anyhow::ensure!(
4826 trade.order_side == plan.order.order_side(),
4827 "Finalized Swap side {} does not match {} order",
4828 trade.order_side,
4829 plan.order.order_side()
4830 );
4831 let gas_cost = included
4832 .receipt
4833 .effective_gas_price
4834 .checked_mul(U256::from(included.receipt.gas_used))
4835 .ok_or_else(|| anyhow::anyhow!("Finalized transaction gas commission overflow"))?;
4836 let commission = Money::from_u256(gas_cost, plan.pool.chain.native_currency())?;
4837 let trade_digest = keccak256(format!("{}:{}", included.tx_hash, swap.log_index));
4838 let trade_digest = trade_digest.to_string();
4839 let trade_id = TradeId::new_checked(&trade_digest[2..38])?;
4840
4841 if plan
4842 .order
4843 .trade_ids()
4844 .iter()
4845 .any(|existing| **existing == trade_id)
4846 {
4847 return Ok(None);
4848 }
4849
4850 let venue_order_id = VenueOrderId::new_checked(included.tx_hash.to_string())?;
4851 let ts_event = UnixNanos::from(timestamp_ns);
4852 let last_px = match plan.order.order_side() {
4853 OrderSide::Buy => fill_price_from_quote(last_qty, plan.amount_in, plan.quote_currency)?,
4854 _ => trade.execution_price,
4855 };
4856 Ok(Some(FinalizedSwapFill {
4857 venue_order_id,
4858 trade_id,
4859 last_qty,
4860 last_px,
4861 commission,
4862 ts_event,
4863 }))
4864}
4865
4866async fn load_verified_wallet_after_fill(
4867 plan: &SwapPlan,
4868 included: &IncludedTransaction,
4869 executor: &TransactionExecutor,
4870) -> anyhow::Result<VerifiedWalletRefresh> {
4871 let mut token_universe = executor.wallet_balance.lock().token_universe.clone();
4872 token_universe.insert(plan.pool.token0.address);
4873 token_universe.insert(plan.pool.token1.address);
4874
4875 let native_amount = required_verification(
4876 executor
4877 .verification
4878 .verify_balance(&executor.wallet_address, included.block_number)
4879 .await,
4880 "finalized native balance",
4881 )?;
4882 let native_balance = Money::from_u256(native_amount.value, plan.pool.chain.native_currency())?;
4883 let mut decisions = vec![verification_decision(
4884 &native_amount,
4885 Some(included.block_number),
4886 Some(included.block_number),
4887 )];
4888 let mut token_addresses = token_universe.iter().copied().collect::<Vec<_>>();
4889 token_addresses.sort_unstable();
4890 let mut token_balances = Vec::with_capacity(token_addresses.len());
4891 for address in token_addresses {
4892 let identities = executor
4893 .deployment_manifest
4894 .tokens
4895 .iter()
4896 .filter(|identity| Address::from_str(&identity.address).ok() == Some(address))
4897 .collect::<Vec<_>>();
4898 anyhow::ensure!(
4899 identities.len() == 1,
4900 "Wallet token {address} does not have exactly one deployment manifest identity"
4901 );
4902 let identity = identities[0];
4903 let token = Token::new(
4904 Arc::clone(&plan.pool.chain),
4905 address,
4906 identity.name.clone(),
4907 identity.symbol.clone(),
4908 identity.decimals,
4909 );
4910 let call = ERC20::balanceOfCall {
4911 account: executor.wallet_address,
4912 }
4913 .abi_encode();
4914 let amount = required_verification(
4915 executor
4916 .verification
4917 .verify_decoded_call(
4918 None,
4919 &address,
4920 U256::ZERO,
4921 &call,
4922 included.block_number,
4923 |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
4924 )
4925 .await,
4926 "finalized token balance",
4927 )?;
4928 decisions.push(verification_decision(
4929 &amount,
4930 Some(included.block_number),
4931 Some(included.block_number),
4932 ));
4933 token_balances.push(TokenBalance::new(amount.value, token));
4934 }
4935
4936 let mut wallet_balance = WalletBalance::new(token_universe);
4937 let balances = wallet_balance.replace_balances(native_balance, token_balances)?;
4938 Ok(VerifiedWalletRefresh {
4939 wallet_balance,
4940 balances,
4941 decisions,
4942 })
4943}
4944
4945async fn verify_connect_capabilities(
4946 verification: &VerificationCoordinator,
4947 manifest: &BlockchainDeploymentManifest,
4948 wallet: Address,
4949 weth: Address,
4950 block: u64,
4951) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
4952 let contract = manifest
4953 .contracts
4954 .first()
4955 .ok_or_else(|| anyhow::anyhow!("Deployment manifest has no capability probe target"))?;
4956 let contract_address = Address::from_str(&contract.address)
4957 .with_context(|| "Deployment manifest capability probe target is invalid")?;
4958 let storage = required_verification(
4959 verification
4960 .verify_storage(&contract_address, &B256::ZERO, block)
4961 .await,
4962 "Blockchain explicit-height storage capability",
4963 )?;
4964
4965 let balance_call = ERC20::balanceOfCall { account: wallet }.abi_encode();
4966 let gas = required_verification(
4967 verification
4968 .verify_gas_estimate(&wallet, &weth, U256::ZERO, &balance_call, block)
4969 .await,
4970 "Blockchain explicit-height gas capability",
4971 )?;
4972
4973 let mut decisions = vec![
4974 verification_decision(&storage, Some(block), Some(block)),
4975 verification_decision(&gas, Some(block), Some(block)),
4976 ];
4977
4978 for pool in &manifest.pools {
4979 let quote_contract = Address::from_str(&pool.quote_contract)
4980 .with_context(|| "Deployment manifest quote capability target is invalid")?;
4981 let token_in = Address::from_str(&pool.token0)
4982 .with_context(|| "Deployment manifest quote input token is invalid")?;
4983 let token_out = Address::from_str(&pool.token1)
4984 .with_context(|| "Deployment manifest quote output token is invalid")?;
4985 let fee = U24::try_from(pool.fee)
4986 .map_err(|_| anyhow::anyhow!("Deployment manifest pool fee is invalid"))?;
4987 let quote = required_verification(
4988 verification
4989 .verify_quote_exact_input_single(
4990 "e_contract,
4991 token_in,
4992 token_out,
4993 U256::from(1u64),
4994 fee,
4995 block,
4996 )
4997 .await,
4998 "Blockchain explicit-height quote capability",
4999 )?;
5000 decisions.push(verification_decision("e, Some(block), Some(block)));
5001 }
5002
5003 let trace = required_verification(
5004 verification.verify_call_trace_capability().await,
5005 "Blockchain call trace capability",
5006 )?;
5007 decisions.push(verification_decision(&trace, None, None));
5008 Ok(decisions)
5009}
5010
5011async fn check_swap_preconditions(
5016 plan: &SwapPlan,
5017 block: u64,
5018 executor: &TransactionExecutor,
5019) -> anyhow::Result<Vec<ExecutionVerificationDecision>> {
5020 let mut decisions = Vec::new();
5021 let factory_call = UniswapV3RouterState::factoryCall.abi_encode();
5022 let router_factory = required_verification(
5023 executor
5024 .verification
5025 .verify_decoded_call(
5026 None,
5027 &plan.router,
5028 U256::ZERO,
5029 &factory_call,
5030 block,
5031 |result| {
5032 UniswapV3RouterState::factoryCall::abi_decode_returns(result)
5033 .map_err(Into::into)
5034 },
5035 )
5036 .await,
5037 "swap router factory",
5038 )?;
5039 anyhow::ensure!(
5040 router_factory.value == plan.factory,
5041 "Router {} reports an unexpected factory",
5042 plan.router,
5043 );
5044 decisions.push(verification_decision(
5045 &router_factory,
5046 Some(block),
5047 Some(block),
5048 ));
5049
5050 let weth_call = UniswapV3RouterState::WETH9Call.abi_encode();
5051 let router_weth = required_verification(
5052 executor
5053 .verification
5054 .verify_decoded_call(
5055 None,
5056 &plan.router,
5057 U256::ZERO,
5058 &weth_call,
5059 block,
5060 |result| {
5061 UniswapV3RouterState::WETH9Call::abi_decode_returns(result).map_err(Into::into)
5062 },
5063 )
5064 .await,
5065 "swap router wrapped native",
5066 )?;
5067 anyhow::ensure!(
5068 router_weth.value == plan.weth,
5069 "Router {} reports an unexpected wrapped native contract",
5070 plan.router,
5071 );
5072 decisions.push(verification_decision(
5073 &router_weth,
5074 Some(block),
5075 Some(block),
5076 ));
5077
5078 let pool_call = UniswapV3Factory::getPoolCall {
5079 tokenA: plan.token_in,
5080 tokenB: plan.token_out,
5081 fee: plan.fee,
5082 }
5083 .abi_encode();
5084 let registered_pool = required_verification(
5085 executor
5086 .verification
5087 .verify_decoded_call(
5088 None,
5089 &plan.factory,
5090 U256::ZERO,
5091 &pool_call,
5092 block,
5093 |result| {
5094 UniswapV3Factory::getPoolCall::abi_decode_returns(result).map_err(Into::into)
5095 },
5096 )
5097 .await,
5098 "swap factory pool",
5099 )?;
5100 anyhow::ensure!(
5101 registered_pool.value == plan.pool_address,
5102 "Factory resolves an unexpected pool for the swap token pair and fee"
5103 );
5104 decisions.push(verification_decision(
5105 ®istered_pool,
5106 Some(block),
5107 Some(block),
5108 ));
5109
5110 for token in [&plan.pool.token0, &plan.pool.token1] {
5111 let decimals_call = ERC20::decimalsCall.abi_encode();
5112 let decimals = required_verification(
5113 executor
5114 .verification
5115 .verify_decoded_call(
5116 None,
5117 &token.address,
5118 U256::ZERO,
5119 &decimals_call,
5120 block,
5121 |result| ERC20::decimalsCall::abi_decode_returns(result).map_err(Into::into),
5122 )
5123 .await,
5124 "swap token decimals",
5125 )?;
5126 anyhow::ensure!(
5127 decimals.value == token.decimals,
5128 "Token {} reports unexpected decimals",
5129 token.address,
5130 );
5131 decisions.push(verification_decision(&decimals, Some(block), Some(block)));
5132 }
5133
5134 let allowance_call = ERC20::allowanceCall {
5135 owner: executor.wallet_address,
5136 spender: plan.router,
5137 }
5138 .abi_encode();
5139 let allowance = required_verification(
5140 executor
5141 .verification
5142 .verify_decoded_call(
5143 None,
5144 &plan.token_in,
5145 U256::ZERO,
5146 &allowance_call,
5147 block,
5148 |result| ERC20::allowanceCall::abi_decode_returns(result).map_err(Into::into),
5149 )
5150 .await,
5151 "swap input allowance",
5152 )?;
5153
5154 if allowance.value < plan.amount_in {
5155 anyhow::bail!(
5156 "Router allowance {} is below the swap amount {} for input token {}; approve the router explicitly before submitting",
5157 allowance.value,
5158 plan.amount_in,
5159 plan.token_in
5160 );
5161 }
5162 decisions.push(verification_decision(&allowance, Some(block), Some(block)));
5163
5164 let balance_call = ERC20::balanceOfCall {
5165 account: executor.wallet_address,
5166 }
5167 .abi_encode();
5168 let balance = required_verification(
5169 executor
5170 .verification
5171 .verify_decoded_call(
5172 None,
5173 &plan.token_in,
5174 U256::ZERO,
5175 &balance_call,
5176 block,
5177 |result| ERC20::balanceOfCall::abi_decode_returns(result).map_err(Into::into),
5178 )
5179 .await,
5180 "swap input balance",
5181 )?;
5182
5183 if balance.value < plan.amount_in {
5184 anyhow::bail!(
5185 "Input token {} balance {} is below the swap amount {}",
5186 plan.token_in,
5187 balance.value,
5188 plan.amount_in
5189 );
5190 }
5191 decisions.push(verification_decision(&balance, Some(block), Some(block)));
5192
5193 Ok(decisions)
5194}
5195
5196fn quantity_to_raw_amount(quantity: Quantity, decimals: u8) -> anyhow::Result<U256> {
5203 if quantity.is_zero() {
5204 anyhow::bail!("Order quantity must be positive");
5205 }
5206
5207 let raw = U256::from(quantity.raw());
5208 let raw_precision = quantity.precision.max(FIXED_PRECISION);
5209 if decimals >= raw_precision {
5210 let scale = U256::from(10u64)
5211 .checked_pow(U256::from(decimals - raw_precision))
5212 .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5213 raw.checked_mul(scale).ok_or_else(|| {
5214 anyhow::anyhow!("Order amount overflow scaling quantity to raw token units")
5215 })
5216 } else {
5217 let divisor = U256::from(10u64)
5218 .checked_pow(U256::from(raw_precision - decimals))
5219 .ok_or_else(|| anyhow::anyhow!("Order amount scaling overflow"))?;
5220 if !(raw % divisor).is_zero() {
5221 anyhow::bail!(
5222 "Order quantity {quantity} is not exactly representable in {decimals} base token decimals"
5223 );
5224 }
5225 Ok(raw / divisor)
5226 }
5227}
5228
5229fn swap_token_pair(
5230 side: OrderSide,
5231 base: Address,
5232 quote: Address,
5233) -> anyhow::Result<(Address, Address)> {
5234 match side {
5235 OrderSide::Sell => Ok((base, quote)),
5236 OrderSide::Buy => Ok((quote, base)),
5237 }
5238}
5239
5240fn fill_price_from_quote(
5241 last_qty: Quantity,
5242 quote_amount: U256,
5243 quote_currency: Currency,
5244) -> anyhow::Result<Price> {
5245 let quote = Money::from_u256(quote_amount, quote_currency)?;
5246 Price::from_decimal_dp(quote.as_decimal() / last_qty.as_decimal(), FIXED_PRECISION)
5247 .map_err(anyhow::Error::from)
5248}
5249
5250fn raw_amount_to_quantity(amount: U256, decimals: u8) -> anyhow::Result<Quantity> {
5251 if amount.is_zero() {
5252 anyhow::bail!("Executed amount must be positive");
5253 }
5254 let quantity = if decimals >= FIXED_PRECISION {
5255 let scale = U256::from(10u64)
5256 .checked_pow(U256::from(decimals - FIXED_PRECISION))
5257 .ok_or_else(|| anyhow::anyhow!("Executed amount scaling overflow"))?;
5258 Quantity::from_u256(amount / scale, FIXED_PRECISION).map_err(anyhow::Error::from)?
5259 } else {
5260 Quantity::from_u256(amount, decimals).map_err(anyhow::Error::from)?
5261 };
5262
5263 if quantity.is_zero() {
5264 anyhow::bail!(
5265 "Executed amount {amount} is below representable quantity precision {FIXED_PRECISION}"
5266 );
5267 }
5268 Ok(quantity)
5269}
5270
5271fn exact_output_amount(quote: &SwapQuote, zero_for_one: bool) -> anyhow::Result<U256> {
5272 let amount = if zero_for_one {
5273 quote.amount1
5274 } else {
5275 quote.amount0
5276 };
5277
5278 if !amount.is_negative() {
5279 anyhow::bail!("Swap quote output amount {amount} is not a positive output");
5280 }
5281 Ok(amount.unsigned_abs())
5282}
5283
5284fn derive_min_amount_out(quoted_amount_out: U256, slippage_bps: u32) -> anyhow::Result<U256> {
5288 if slippage_bps >= BPS_DENOMINATOR {
5289 anyhow::bail!("Slippage {slippage_bps} bps must be below {BPS_DENOMINATOR}");
5290 }
5291 let min_amount_out = quoted_amount_out
5292 .checked_mul(U256::from(BPS_DENOMINATOR - slippage_bps))
5293 .and_then(|scaled| scaled.checked_div(U256::from(BPS_DENOMINATOR)))
5294 .ok_or_else(|| anyhow::anyhow!("Minimum output derivation overflow"))?;
5295 if min_amount_out.is_zero() {
5296 anyhow::bail!(
5297 "Derived minimum output is zero for quoted output {quoted_amount_out} at {slippage_bps} bps slippage"
5298 );
5299 }
5300 Ok(min_amount_out)
5301}
5302
5303impl BlockchainExecutionClient {
5304 async fn build_execution_verification_migration(
5305 &self,
5306 snapshot: ExecutionVerificationMigrationSnapshot,
5307 finalized: VerifiedBlockHeader,
5308 finalized_headers: &[VerifiedBlockHeader],
5309 nonce_verification: &Verified<u64>,
5310 ) -> anyhow::Result<ExecutionVerificationMigration> {
5311 let next_canonical_nonce = nonce_verification.value;
5312 let mut hashes_by_intent: HashMap<i64, Vec<&ExecutionTransactionHashRow>> = HashMap::new();
5313 for hash in &snapshot.hashes {
5314 hashes_by_intent
5315 .entry(hash.intent_id)
5316 .or_default()
5317 .push(hash);
5318 }
5319 let active_count = snapshot
5320 .intents
5321 .iter()
5322 .filter(|intent| intent.active)
5323 .count();
5324 anyhow::ensure!(
5325 active_count <= 1,
5326 "Retained execution history has multiple active signer owners"
5327 );
5328
5329 let mut nonce_owners = HashMap::new();
5330 let mut records = Vec::with_capacity(snapshot.intents.len());
5331 let finalized_headers = finalized_headers
5332 .iter()
5333 .map(durable_verified_header)
5334 .collect::<Vec<_>>();
5335
5336 for intent in &snapshot.intents {
5337 anyhow::ensure!(
5338 intent.chain_id == self.chain.chain_id
5339 && intent.wallet_address == self.config.wallet_address,
5340 "Retained execution intent belongs to another signer"
5341 );
5342 let purpose = TransactionPurpose::parse(&intent.purpose).ok_or_else(|| {
5343 anyhow::anyhow!("Retained execution intent has an unsupported purpose")
5344 })?;
5345 let hashes = hashes_by_intent
5346 .get(&intent.id)
5347 .map(Vec::as_slice)
5348 .unwrap_or_default();
5349 let current = hashes
5350 .iter()
5351 .copied()
5352 .filter(|hash| hash.current)
5353 .collect::<Vec<_>>();
5354 anyhow::ensure!(
5355 current.len() <= 1,
5356 "Retained execution intent {} has multiple current hashes",
5357 intent.id
5358 );
5359 let current = current.first().copied();
5360 let mut authenticated = HashMap::new();
5361
5362 for hash in hashes {
5363 if hash.payload_expected {
5364 let raw = open_execution_payload(
5365 self.payload_keys
5366 .as_deref()
5367 .expect("Postgres execution requires payload keys"),
5368 self.payload_policy(),
5369 intent,
5370 hash,
5371 "verification migration",
5372 )?;
5373 authenticated.insert(hash.id, raw);
5374 } else {
5375 anyhow::ensure!(
5376 hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
5377 "Unowned replacement hash retains signed bytes"
5378 );
5379 }
5380 }
5381
5382 if let Some(nonce) = intent.nonce {
5383 anyhow::ensure!(
5384 nonce_owners.insert(nonce, intent.id).is_none(),
5385 "Retained execution history has duplicate signer nonce ownership"
5386 );
5387 }
5388
5389 let base_decision = verification_decision(
5390 nonce_verification,
5391 Some(finalized.number),
5392 Some(finalized.number),
5393 );
5394
5395 if !intent.active {
5396 if matches!(intent.status.as_str(), "finalized" | "reverted") {
5397 let expected_marker =
5398 if purpose == TransactionPurpose::Swap && intent.status == "finalized" {
5399 intent.fill_emitted
5400 } else {
5401 intent.terminal_emitted
5402 };
5403 anyhow::ensure!(
5404 expected_marker,
5405 "Released terminal intent {} has no durable event marker",
5406 intent.id
5407 );
5408 } else {
5409 anyhow::ensure!(
5410 matches!(intent.status.as_str(), "dropped" | "recoverable")
5411 && authenticated.is_empty(),
5412 "Released nonterminal intent {} retains signed ownership",
5413 intent.id
5414 );
5415 records.push(ExecutionVerificationMigrationRecord {
5416 intent_id: intent.id,
5417 nonce: intent.nonce,
5418 transaction_hash: None,
5419 terminal_status: None,
5420 block_number: None,
5421 block_hash: None,
5422 receipt_success: None,
5423 gas_used: None,
5424 effective_gas_price: None,
5425 recover_prepared: false,
5426 decisions: vec![base_decision],
5427 });
5428 continue;
5429 }
5430 }
5431
5432 if intent.active && intent.nonce.is_none() {
5433 anyhow::ensure!(
5434 intent.status == "prepared" && hashes.is_empty(),
5435 "Unassigned active intent {} is not an unsigned preparation",
5436 intent.id
5437 );
5438 records.push(ExecutionVerificationMigrationRecord {
5439 intent_id: intent.id,
5440 nonce: None,
5441 transaction_hash: None,
5442 terminal_status: None,
5443 block_number: None,
5444 block_hash: None,
5445 receipt_success: None,
5446 gas_used: None,
5447 effective_gas_price: None,
5448 recover_prepared: true,
5449 decisions: vec![base_decision],
5450 });
5451 continue;
5452 }
5453
5454 let nonce = intent.nonce.ok_or_else(|| {
5455 anyhow::anyhow!("Retained signed intent {} has no nonce", intent.id)
5456 })?;
5457 let current = current.ok_or_else(|| {
5458 anyhow::anyhow!("Retained signed intent {} has no current hash", intent.id)
5459 })?;
5460 let raw_transaction = authenticated.get(¤t.id).ok_or_else(|| {
5461 anyhow::anyhow!(
5462 "Retained signed intent {} has no authenticated current payload",
5463 intent.id
5464 )
5465 })?;
5466
5467 if intent.active && nonce == next_canonical_nonce {
5468 anyhow::ensure!(
5469 !matches!(intent.status.as_str(), "finalized" | "reverted"),
5470 "Active terminal intent conflicts with the canonical nonce ledger"
5471 );
5472 records.push(ExecutionVerificationMigrationRecord {
5473 intent_id: intent.id,
5474 nonce: Some(nonce),
5475 transaction_hash: Some(current.transaction_hash.clone()),
5476 terminal_status: None,
5477 block_number: None,
5478 block_hash: None,
5479 receipt_success: None,
5480 gas_used: None,
5481 effective_gas_price: None,
5482 recover_prepared: false,
5483 decisions: vec![base_decision],
5484 });
5485 continue;
5486 }
5487 anyhow::ensure!(
5488 nonce < next_canonical_nonce,
5489 "Retained active nonce {nonce} is above canonical nonce {next_canonical_nonce}"
5490 );
5491
5492 let receipt_verification = required_verification(
5493 self.verification
5494 .verify_receipt(&B256::from_str(¤t.transaction_hash).with_context(
5495 || {
5496 format!(
5497 "Retained transaction hash {} is invalid",
5498 current.transaction_hash
5499 )
5500 },
5501 )?)
5502 .await,
5503 "migration receipt",
5504 )?;
5505 let receipt = receipt_verification.value.clone();
5506 anyhow::ensure!(
5507 receipt.block_number <= finalized.number,
5508 "Retained terminal receipt is above the verified finalized boundary"
5509 );
5510 let inclusion_verification = required_verification(
5511 self.verification.verify_block(receipt.block_number).await,
5512 "migration inclusion header",
5513 )?;
5514 anyhow::ensure!(
5515 inclusion_verification.value.hash == receipt.block_hash
5516 && finalized_headers.iter().any(|header| {
5517 header.number == receipt.block_number
5518 && header.hash == receipt.block_hash.to_string()
5519 }),
5520 "Retained terminal receipt is not on the verified finalized ancestry"
5521 );
5522 let tx_hash = B256::from_str(¤t.transaction_hash)
5523 .context("Retained transaction hash is invalid")?;
5524 let included = IncludedTransaction {
5525 intent_id: intent.id,
5526 nonce,
5527 tx_hash,
5528 block_number: receipt.block_number,
5529 receipt: receipt.clone(),
5530 finality: StableFinality {
5531 decisions: Vec::new(),
5532 inclusion_header: durable_verified_header(&inclusion_verification.value),
5533 finalized_headers: finalized_headers.clone(),
5534 },
5535 };
5536 let trace_purpose = match purpose {
5537 TransactionPurpose::Wrap => "wrap",
5538 TransactionPurpose::Approve => "approve",
5539 TransactionPurpose::Swap => {
5540 match self.restore_swap_plan(intent)?.order.order_side() {
5541 OrderSide::Sell => "swap_sell",
5542 OrderSide::Buy => "swap_buy",
5543 }
5544 }
5545 };
5546 let mut decisions = vec![
5547 verification_decision(
5548 &receipt_verification,
5549 Some(receipt.block_number),
5550 Some(receipt.block_number),
5551 ),
5552 verification_decision(
5553 &inclusion_verification,
5554 Some(receipt.block_number),
5555 Some(receipt.block_number),
5556 ),
5557 ];
5558 decisions.extend(
5559 verify_finalized_transaction_identity(
5560 &included,
5561 intent,
5562 nonce,
5563 raw_transaction,
5564 &self.verification,
5565 self.wallet_address,
5566 self.chain.chain_id,
5567 &self
5568 .config
5569 .verification
5570 .as_ref()
5571 .expect("verification config validated")
5572 .deployment_manifest,
5573 trace_purpose,
5574 )
5575 .await?,
5576 );
5577 let terminal_status = if receipt.status {
5578 TransactionStatus::Finalized
5579 } else {
5580 TransactionStatus::Reverted
5581 };
5582
5583 if !intent.active {
5584 anyhow::ensure!(
5585 intent.status == terminal_status.as_str(),
5586 "Released terminal intent status conflicts with its verified receipt"
5587 );
5588 }
5589 records.push(ExecutionVerificationMigrationRecord {
5590 intent_id: intent.id,
5591 nonce: Some(nonce),
5592 transaction_hash: Some(current.transaction_hash.clone()),
5593 terminal_status: Some(terminal_status),
5594 block_number: Some(receipt.block_number),
5595 block_hash: Some(receipt.block_hash.to_string()),
5596 receipt_success: Some(receipt.status),
5597 gas_used: Some(receipt.gas_used),
5598 effective_gas_price: Some(receipt.effective_gas_price.to_string()),
5599 recover_prepared: false,
5600 decisions,
5601 });
5602 }
5603
5604 Ok(ExecutionVerificationMigration { snapshot, records })
5605 }
5606}
5607
5608#[async_trait(?Send)]
5609impl ExecutionClient for BlockchainExecutionClient {
5610 fn is_connected(&self) -> bool {
5611 self.core.is_connected()
5612 }
5613
5614 fn client_id(&self) -> ClientId {
5615 self.core.client_id
5616 }
5617
5618 fn account_id(&self) -> AccountId {
5619 self.core.account_id
5620 }
5621
5622 fn venue(&self) -> Venue {
5623 self.core.venue
5624 }
5625
5626 fn handles_order_venue(&self, venue: Venue) -> bool {
5627 venue.parse_dex().is_ok_and(|(blockchain, dex_type)| {
5628 blockchain == self.chain.name && dex_type == DexType::UniswapV3
5629 })
5630 }
5631
5632 fn oms_type(&self) -> OmsType {
5633 self.core.oms_type
5634 }
5635
5636 fn get_account(&self) -> Option<AccountAny> {
5637 self.core.cache().account_owned(&self.core.account_id)
5638 }
5639
5640 fn generate_account_state(
5641 &self,
5642 balances: Vec<AccountBalance>,
5643 margins: Vec<MarginBalance>,
5644 reported: bool,
5645 ts_event: UnixNanos,
5646 info: Option<Params>,
5647 ) -> anyhow::Result<()> {
5648 self.emitter
5649 .try_emit_account_state(balances, margins, reported, ts_event, info)
5650 }
5651
5652 fn start(&mut self) -> anyhow::Result<()> {
5653 if self.core.is_started() {
5654 return Ok(());
5655 }
5656
5657 self.emitter.set_sender(get_exec_event_sender());
5658 self.core.set_started();
5659 log::info!(
5660 "Started: client_id={}, account_id={}",
5661 self.core.client_id,
5662 self.core.account_id
5663 );
5664 Ok(())
5665 }
5666
5667 fn stop(&mut self) -> anyhow::Result<()> {
5668 if self.core.is_stopped() {
5669 return Ok(());
5670 }
5671
5672 self.pending_tasks.begin_shutdown();
5673 self.signer = None;
5674 self.core.set_stopped();
5675 self.core.set_disconnected();
5676 log::info!("Stopped: client_id={}", self.core.client_id);
5677 Ok(())
5678 }
5679
5680 fn submit_order(&self, cmd: SubmitOrder) -> anyhow::Result<()> {
5681 let order = self.core.cache().try_order_owned(&cmd.client_order_id)?;
5682
5683 if order.is_closed() {
5684 log::warn!("Cannot submit closed order {}", order.client_order_id());
5685 return Ok(());
5686 }
5687
5688 if let Err(reason) = validate_order(&order) {
5689 self.emitter.emit_order_denied(&order, &reason.to_string());
5690 return Ok(());
5691 }
5692
5693 if !self.pending_tasks.is_open() {
5694 self.emitter
5695 .emit_order_denied(&order, "Blockchain execution client is shutting down");
5696 return Ok(());
5697 }
5698
5699 let plan = match self.prepare_swap(&cmd, &order) {
5700 Ok(plan) => plan,
5701 Err(e) => {
5702 self.emitter.emit_order_denied(&order, &e.to_string());
5703 return Ok(());
5704 }
5705 };
5706
5707 let executor = match self.transaction_executor() {
5708 Ok(executor) => executor,
5709 Err(e) => {
5710 self.emitter.emit_order_denied(&order, &e.to_string());
5711 return Ok(());
5712 }
5713 };
5714
5715 let emitter = self.emitter.clone();
5716 let max_quote_age_blocks = self.transaction_limits.max_quote_age_blocks;
5717 let deadline_seconds = self.transaction_limits.deadline_seconds;
5718 let client_order_id = order.client_order_id();
5719
5720 let future = async move {
5721 if let Err(e) = execute_swap(
5722 plan,
5723 executor,
5724 emitter,
5725 max_quote_age_blocks,
5726 deadline_seconds,
5727 )
5728 .await
5729 {
5730 log::warn!("Swap execution for order {client_order_id} failed: {e:?}");
5731 }
5732 };
5733
5734 if let Err(e) = self.pending_tasks.spawn(future) {
5735 release_preparing_slot(&self.in_flight);
5736 log::warn!("Skipping blockchain swap after shutdown began: {e}");
5737 }
5738
5739 Ok(())
5740 }
5741
5742 fn submit_order_list(&self, cmd: SubmitOrderList) -> anyhow::Result<()> {
5743 let orders = self
5744 .core
5745 .cache()
5746 .orders_for_ids(&cmd.order_list.client_order_ids, &cmd);
5747
5748 for order in &orders {
5749 if order.is_closed() {
5750 log::warn!("Cannot submit closed order {}", order.client_order_id());
5751 continue;
5752 }
5753
5754 self.emitter
5755 .emit_order_denied(order, ORDER_LIST_UNSUPPORTED);
5756 }
5757
5758 Ok(())
5759 }
5760
5761 fn modify_order(&self, cmd: ModifyOrder) -> anyhow::Result<()> {
5762 let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5763 log::warn!("Cannot modify unknown order {}", cmd.client_order_id);
5764 return Ok(());
5765 };
5766
5767 self.emitter.emit_order_modify_rejected(
5768 &order,
5769 cmd.venue_order_id,
5770 ORDER_MODIFY_UNSUPPORTED,
5771 get_atomic_clock_realtime().get_time_ns(),
5772 );
5773 Ok(())
5774 }
5775
5776 fn cancel_order(&self, cmd: CancelOrder) -> anyhow::Result<()> {
5777 let Ok(order) = self.core.cache().try_order_owned(&cmd.client_order_id) else {
5778 log::warn!("Cannot cancel unknown order {}", cmd.client_order_id);
5779 return Ok(());
5780 };
5781
5782 self.emitter.emit_order_cancel_rejected(
5783 &order,
5784 cmd.venue_order_id,
5785 ORDER_CANCEL_UNSUPPORTED,
5786 get_atomic_clock_realtime().get_time_ns(),
5787 );
5788 Ok(())
5789 }
5790
5791 fn cancel_all_orders(&self, cmd: CancelAllOrders) -> anyhow::Result<()> {
5792 log::warn!(
5793 "Cancel-all for {} is not supported on the blockchain execution client",
5794 cmd.instrument_id
5795 );
5796 Ok(())
5797 }
5798
5799 fn batch_cancel_orders(&self, cmd: BatchCancelOrders) -> anyhow::Result<()> {
5800 for cancel in cmd.cancels {
5801 self.cancel_order(cancel)?;
5802 }
5803 Ok(())
5804 }
5805
5806 fn query_account(&self, cmd: QueryAccount) -> anyhow::Result<()> {
5807 anyhow::ensure!(
5808 cmd.account_id == self.core.account_id,
5809 "Query account ID {} does not match client account ID {}",
5810 cmd.account_id,
5811 self.core.account_id
5812 );
5813 anyhow::ensure!(self.core.is_started(), "Execution client is not started");
5814
5815 let balances = self.wallet_balance.lock().as_account_balances()?;
5816 self.generate_account_state(
5817 balances,
5818 vec![],
5819 true,
5820 get_atomic_clock_realtime().get_time_ns(),
5821 None,
5822 )
5823 }
5824
5825 fn query_order(&self, cmd: QueryOrder) -> anyhow::Result<()> {
5826 log::debug!(
5827 "Order queries are not supported on the blockchain execution client; cannot query {}",
5828 cmd.client_order_id
5829 );
5830 Ok(())
5831 }
5832
5833 fn retain_unresolved_submissions(&self) -> bool {
5834 true
5835 }
5836
5837 async fn connect(&mut self) -> anyhow::Result<()> {
5838 if self.core.is_connected() {
5839 log::warn!("Blockchain execution client already connected");
5840 return Ok(());
5841 }
5842
5843 log::info!(
5844 "Connecting to blockchain execution client on chain {}",
5845 self.chain.name
5846 );
5847
5848 if !self.pending_tasks.is_open() || !self.pending_tasks.is_empty() {
5849 self.pending_tasks.begin_shutdown();
5850 self.pending_tasks
5851 .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
5852 .await
5853 .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
5854 self.signer = None;
5855 self.pending_tasks
5856 .start_generation()
5857 .map_err(|e| anyhow::anyhow!("Failed to start blockchain task generation: {e}"))?;
5858 }
5859 release_preparing_slot(&self.in_flight);
5860
5861 let setup_guard = TaskGroupGuard::new(&[&self.pending_tasks], || {});
5862
5863 let payload_keys = PayloadKeySet::load(
5864 self.config.payload_key_env.as_deref(),
5865 &self.config.payload_key_retired_env,
5866 self.config.payload_deployment_id.as_deref(),
5867 )?
5868 .map(Arc::new);
5869
5870 if self.cache.database.is_some() || self.config.postgres_cache_database_config.is_some() {
5871 let keys = payload_keys.as_deref().ok_or_else(|| {
5872 anyhow::anyhow!(
5873 "Postgres execution requires an active payload key and deployment identity"
5874 )
5875 })?;
5876
5877 if self.cache.database.is_none() {
5878 let pg_options = self
5879 .config
5880 .postgres_cache_database_config
5881 .as_ref()
5882 .expect("Postgres configuration checked above");
5883 let database = crate::cache::database::BlockchainCacheDatabase::connect(
5884 pg_options.clone().into(),
5885 )
5886 .await
5887 .map_err(|e| {
5888 anyhow::anyhow!("Failed to connect to the Postgres cache database: {e}")
5889 })?;
5890 self.cache.database = Some(database);
5891 }
5892 self.cache
5893 .database
5894 .as_ref()
5895 .expect("database was attached")
5896 .require_execution_payload_storage_ready(keys)
5897 .await?;
5898 self.cache.initialize_chain().await;
5899 self.cache.ensure_execution_transaction_schema().await?;
5900 let check = self
5901 .cache
5902 .database
5903 .as_ref()
5904 .expect("database was attached")
5905 .check_execution_payload_storage(
5906 Some(keys),
5907 Some(PayloadPolicy {
5908 chain_id: self.chain.chain_id,
5909 signer: self.wallet_address,
5910 gas_limit: self.config.gas_limit,
5911 max_fee_per_gas: self.config.max_fee_per_gas_wei,
5912 }),
5913 100,
5914 )
5915 .await?;
5916 anyhow::ensure!(
5917 check.protected,
5918 "Postgres execution requires protected payload storage"
5919 );
5920 } else {
5921 log::warn!(
5922 "No Postgres cache database configured; transactions will be refused (no durable store)"
5923 );
5924 }
5925 self.payload_keys = payload_keys;
5926
5927 let verification = self
5928 .config
5929 .verification
5930 .as_ref()
5931 .expect("verification config validated at construction");
5932 let position = if let Some(database) = self.cache.database.as_ref() {
5933 database
5934 .load_execution_verification_position(
5935 self.chain.chain_id,
5936 &self.config.wallet_address,
5937 &verification.manifest_version,
5938 &verification.manifest_digest,
5939 )
5940 .await?
5941 } else {
5942 None
5943 };
5944 let migration_snapshot = if position.is_none() {
5945 if let Some(database) = self.cache.database.as_ref() {
5946 let snapshot = database
5947 .load_execution_verification_migration_snapshot(
5948 self.chain.chain_id,
5949 &self.config.wallet_address,
5950 )
5951 .await?;
5952
5953 if snapshot.intents.is_empty() {
5954 None
5955 } else {
5956 Some(snapshot)
5957 }
5958 } else {
5959 None
5960 }
5961 } else {
5962 None
5963 };
5964
5965 let chain_id_verification = required_verification(
5966 self.verification.verify_chain_id().await,
5967 "Blockchain chain ID",
5968 )?;
5969 let checkpoint_verification = required_verification(
5970 self.verification.verify_checkpoint().await,
5971 "Blockchain checkpoint",
5972 )?;
5973 let checkpoint = checkpoint_verification.value;
5974 let finalized_verification = required_verification(
5975 self.verification.verify_finalized_header().await,
5976 "Blockchain finalized header",
5977 )?;
5978 let finalized = finalized_verification.value;
5979 let mut connect_decisions = vec![
5980 verification_decision(&chain_id_verification, None, None),
5981 verification_decision(
5982 &checkpoint_verification,
5983 Some(checkpoint.number),
5984 Some(checkpoint.number),
5985 ),
5986 verification_decision(
5987 &finalized_verification,
5988 Some(finalized.number),
5989 Some(finalized.number),
5990 ),
5991 ];
5992 let mut finalized_headers = if let Some(position) = position.as_ref() {
5993 let durable_tip = parse_verified_header(&position.finalized_tip)?;
5994 anyhow::ensure!(
5995 durable_tip.number >= checkpoint.number,
5996 "Durable finalized header tip precedes the trusted checkpoint"
5997 );
5998 let durable_tip_verification = required_verification(
5999 self.verification.verify_block(durable_tip.number).await,
6000 "Blockchain durable finalized tip",
6001 )?;
6002 anyhow::ensure!(
6003 durable_tip_verification.value == durable_tip,
6004 "Durable finalized header tip conflicts with independent sources"
6005 );
6006 connect_decisions.push(verification_decision(
6007 &durable_tip_verification,
6008 Some(durable_tip.number),
6009 Some(durable_tip.number),
6010 ));
6011 vec![durable_tip]
6012 } else {
6013 vec![checkpoint]
6014 };
6015 let mut ancestry_cursor = *finalized_headers
6016 .last()
6017 .expect("finalized header ledger is nonempty");
6018 anyhow::ensure!(
6019 finalized.number >= ancestry_cursor.number,
6020 "Verified finalized height regressed below the durable finalized header tip"
6021 );
6022
6023 while ancestry_cursor.number < finalized.number {
6024 let end = ancestry_cursor
6025 .number
6026 .saturating_add(4_096)
6027 .min(finalized.number);
6028 let start = ancestry_cursor.number.saturating_add(1);
6029 let headers_verification = required_verification(
6030 self.verification
6031 .verify_header_window(ancestry_cursor, end)
6032 .await,
6033 "Blockchain finalized ancestry",
6034 )?;
6035 let headers = &headers_verification.value;
6036 ancestry_cursor = *headers
6037 .last()
6038 .expect("nonempty ancestry window advances the cursor");
6039 finalized_headers.extend(headers.iter().copied());
6040 connect_decisions.push(verification_decision(
6041 &headers_verification,
6042 Some(start),
6043 Some(end),
6044 ));
6045 }
6046 anyhow::ensure!(
6047 ancestry_cursor == finalized,
6048 "Verified finalized header conflicts with its ancestry window"
6049 );
6050 let nonce_verification = required_verification(
6051 self.verification
6052 .verify_transaction_count(&self.wallet_address, finalized.number)
6053 .await,
6054 "Blockchain finalized transaction count",
6055 )?;
6056 let observed_canonical_nonce = nonce_verification.value;
6057 let next_canonical_nonce = position
6058 .as_ref()
6059 .map_or(observed_canonical_nonce, |position| {
6060 position.next_canonical_nonce
6061 });
6062
6063 if let Some(position) = position.as_ref() {
6064 log::debug!(
6065 "Resumed execution verification ledger at nonce revision {} with observed finalized nonce {}",
6066 position.revision,
6067 observed_canonical_nonce,
6068 );
6069 }
6070 connect_decisions.push(verification_decision(
6071 &nonce_verification,
6072 Some(finalized.number),
6073 Some(finalized.number),
6074 ));
6075 let deployment_verification = required_verification(
6076 self.verification
6077 .verify_deployment_manifest(&verification.deployment_manifest, finalized.number)
6078 .await,
6079 "Blockchain deployment manifest",
6080 )?;
6081 connect_decisions.push(verification_decision(
6082 &deployment_verification,
6083 Some(finalized.number),
6084 Some(finalized.number),
6085 ));
6086 connect_decisions.extend(
6087 verify_connect_capabilities(
6088 &self.verification,
6089 &verification.deployment_manifest,
6090 self.wallet_address,
6091 self.weth_address,
6092 finalized.number,
6093 )
6094 .await?,
6095 );
6096 let migration = if let Some(snapshot) = migration_snapshot {
6097 Some(
6098 self.build_execution_verification_migration(
6099 snapshot,
6100 finalized,
6101 &finalized_headers,
6102 &nonce_verification,
6103 )
6104 .await?,
6105 )
6106 } else {
6107 None
6108 };
6109
6110 if let Some(database) = self.cache.database.as_ref() {
6111 let identities = std::iter::once(&verification.authoritative)
6112 .chain(
6113 verification
6114 .verifiers
6115 .iter()
6116 .map(|provider| &provider.identity),
6117 )
6118 .collect::<Vec<_>>();
6119 let provider_ids = identities
6120 .iter()
6121 .map(|identity| identity.provider_id.clone())
6122 .collect::<Vec<_>>();
6123 let operator_ids = identities
6124 .iter()
6125 .map(|identity| identity.operator_id.clone())
6126 .collect::<Vec<_>>();
6127 let failure_domain_ids = identities
6128 .iter()
6129 .flat_map(|identity| identity.failure_domain_ids.iter().cloned())
6130 .collect::<Vec<_>>();
6131 let finalized_headers = finalized_headers
6132 .iter()
6133 .map(durable_verified_header)
6134 .collect::<Vec<_>>();
6135 database
6136 .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
6137 chain_id: self.chain.chain_id,
6138 wallet_address: &self.config.wallet_address,
6139 manifest_version: &verification.manifest_version,
6140 manifest_digest: &verification.manifest_digest,
6141 checkpoint_number: checkpoint.number,
6142 checkpoint_hash: &checkpoint.hash.to_string(),
6143 checkpoint_parent_hash: &checkpoint.parent_hash.to_string(),
6144 checkpoint_timestamp: checkpoint.timestamp,
6145 checkpoint_base_fee_per_gas: checkpoint.base_fee_per_gas,
6146 finalized_headers: &finalized_headers,
6147 next_canonical_nonce,
6148 observed_canonical_nonce,
6149 provider_ids: &provider_ids,
6150 operator_ids: &operator_ids,
6151 failure_domain_ids: &failure_domain_ids,
6152 decisions: &connect_decisions,
6153 migration: migration.as_ref(),
6154 })
6155 .await?;
6156 }
6157
6158 let payload_connect_lease = if let (Some(database), Some(keys)) =
6159 (self.cache.database.as_ref(), self.payload_keys.as_deref())
6160 {
6161 Some(
6162 database
6163 .require_execution_payload_storage(keys, self.payload_policy(), 100)
6164 .await?,
6165 )
6166 } else {
6167 None
6168 };
6169
6170 let private_key = Zeroizing::new(
6173 std::env::var(&self.config.signer_private_key_env).map_err(|_| {
6174 anyhow::anyhow!(
6175 "Signer private key environment variable '{}' is not set",
6176 self.config.signer_private_key_env
6177 )
6178 })?,
6179 );
6180 let encoded_key = private_key.trim();
6181 let encoded_key = encoded_key.strip_prefix("0x").unwrap_or(encoded_key);
6182 let key_bytes = Zeroizing::new(hex::decode_array::<32>(encoded_key).map_err(|_| {
6183 anyhow::anyhow!(
6184 "Signer private key in '{}' is not a valid hex private key",
6185 self.config.signer_private_key_env
6186 )
6187 })?);
6188 let signer = PrivateKeySigner::from_slice(&key_bytes[..]).map_err(|_| {
6189 anyhow::anyhow!(
6190 "Signer private key in '{}' is not a valid secp256k1 private key",
6191 self.config.signer_private_key_env
6192 )
6193 })?;
6194
6195 if signer.address() != self.wallet_address {
6196 anyhow::bail!(
6197 "Signer address {} derived from '{}' does not match configured wallet address {}",
6198 signer.address(),
6199 self.config.signer_private_key_env,
6200 self.wallet_address
6201 );
6202 }
6203
6204 self.signer = Some(Arc::new(signer));
6205 drop(payload_connect_lease);
6206
6207 if self.cache.has_database()
6208 && let Err(e) = self.reconcile_unresolved_execution().await
6209 {
6210 self.signer = None;
6211 return Err(e);
6212 }
6213
6214 if let Err(e) = self.refresh_wallet_balances().await {
6215 self.signer = None;
6216 return Err(e);
6217 }
6218 self.core.set_connected();
6219 setup_guard.disarm();
6220 log::info!(
6221 "Blockchain execution client connected on chain {}",
6222 self.chain.name
6223 );
6224 Ok(())
6225 }
6226
6227 async fn disconnect(&mut self) -> anyhow::Result<()> {
6228 self.pending_tasks.begin_shutdown();
6229 let tasks_result = self
6230 .pending_tasks
6231 .finish_shutdown(Duration::from_secs(5), Duration::from_secs(2))
6232 .await;
6233 self.signer = None;
6234 self.core.set_disconnected();
6235 tasks_result
6236 .map(|_| ())
6237 .map_err(|e| anyhow::anyhow!("Failed to terminate blockchain submissions: {e}"))?;
6238 Ok(())
6239 }
6240
6241 async fn generate_order_status_report(
6242 &self,
6243 _cmd: &GenerateOrderStatusReport,
6244 ) -> anyhow::Result<Option<OrderStatusReport>> {
6245 anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6246 }
6247
6248 async fn generate_order_status_reports(
6249 &self,
6250 _cmd: &GenerateOrderStatusReports,
6251 ) -> anyhow::Result<Vec<OrderStatusReport>> {
6252 anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6253 }
6254
6255 async fn generate_fill_reports(
6256 &self,
6257 _cmd: GenerateFillReports,
6258 ) -> anyhow::Result<Vec<FillReport>> {
6259 anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6260 }
6261
6262 async fn generate_position_status_reports(
6263 &self,
6264 _cmd: &GeneratePositionStatusReports,
6265 ) -> anyhow::Result<Vec<PositionStatusReport>> {
6266 anyhow::bail!("{VENUE_EXECUTION_REPORTS_UNSUPPORTED}");
6267 }
6268
6269 async fn generate_mass_status(
6270 &self,
6271 _lookback_mins: Option<u64>,
6272 ) -> anyhow::Result<Option<ExecutionMassStatus>> {
6273 log::info!(
6276 "Mass status is not supported on the blockchain execution client; skipping venue reconciliation"
6277 );
6278 Ok(None)
6279 }
6280}
6281
6282fn validate_order(order: &impl Order) -> Result<(), OrderDeniedReason> {
6283 if order.is_reduce_only() {
6284 return Err(OrderDeniedReason::UnsupportedReduceOnly);
6285 }
6286
6287 Ok(())
6288}
6289
6290#[cfg(test)]
6291mod tests {
6292 use std::{
6293 cell::RefCell,
6294 rc::Rc,
6295 sync::atomic::{AtomicU64, Ordering},
6296 };
6297
6298 use alloy::{
6299 primitives::{address, aliases::I24},
6300 sol_types::SolValue,
6301 };
6302 use nautilus_common::{
6303 cache::Cache,
6304 clock::{Clock, VirtualClock},
6305 live::runner::replace_exec_event_sender,
6306 messages::{ExecutionEvent, execution::TradingCommand},
6307 testing::wait_until_async,
6308 };
6309 use nautilus_core::UUID4;
6310 use nautilus_infrastructure::sql::pg::{PostgresConnectOptions, get_postgres_connect_options};
6311 use nautilus_live::{
6312 execution::{
6313 config::ExecutionManagerConfig,
6314 submission::{SubmissionRecoveryExhausted, SubmissionRecoverySource},
6315 },
6316 manager::ExecutionManager,
6317 };
6318 use nautilus_model::{
6319 defi::{
6320 PoolProfiler,
6321 chain::chains,
6322 data::block::BlockPosition,
6323 pool_analysis::{
6324 position::PoolPosition,
6325 snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
6326 },
6327 tick_map::{tick::PoolTick, tick_math::get_tick_at_sqrt_ratio},
6328 },
6329 enums::AccountType,
6330 events::OrderEventAny,
6331 identifiers::{OrderListId, StrategyId, TraderId},
6332 orders::{OrderList, OrderTestBuilder},
6333 types::Price,
6334 };
6335 use rstest::rstest;
6336 use sqlx::postgres::{PgAdvisoryLock, PgAdvisoryLockKey, PgPoolOptions};
6337
6338 use super::*;
6339 use crate::{
6340 cache::database::tests::connect_test_database,
6341 config::{
6342 BlockchainCallEdgeManifest, BlockchainChainAnchorConfig, BlockchainContractManifest,
6343 BlockchainContractProbe, BlockchainContractRole, BlockchainDeploymentManifest,
6344 BlockchainPoolManifest, BlockchainProviderIdentity, BlockchainTokenManifest,
6345 BlockchainVerificationConfig, BlockchainVerificationProviderConfig, QuoteSpendLimit,
6346 },
6347 constants::BLOCKCHAIN_VENUE,
6348 exchanges::arbitrum::UNISWAP_V3,
6349 rpc::http::{
6350 EXECUTION_RPC_TIMEOUT_SECS,
6351 tests::mock::{MockRpcState, start_mock_rpc_server},
6352 },
6353 };
6354
6355 async fn poll_for_receipt(
6358 http_rpc_client: &BlockchainHttpRpcClient,
6359 tx_hash: &B256,
6360 max_polls: u32,
6361 interval: Duration,
6362 ) -> anyhow::Result<Option<RpcTransactionReceipt>> {
6363 let mut last_error = None;
6364 let mut observed_pending = false;
6365
6366 for attempt in 0..max_polls {
6367 if attempt > 0 {
6368 tokio::time::sleep(interval).await;
6369 }
6370
6371 match http_rpc_client.get_transaction_receipt(tx_hash).await {
6372 Ok(Some(receipt)) => return Ok(Some(receipt)),
6373 Ok(None) => observed_pending = true,
6374 Err(e) => {
6375 log::warn!(
6376 "Receipt poll {}/{} for transaction {tx_hash} failed: {e}",
6377 attempt + 1,
6378 max_polls
6379 );
6380 last_error = Some(e);
6381 }
6382 }
6383 }
6384
6385 if !observed_pending && let Some(e) = last_error {
6386 return Err(e);
6387 }
6388
6389 Ok(None)
6390 }
6391
6392 const CHAIN_ID_ARBITRUM: &str =
6393 include_str!("../../test_data/execution/rpc_eth_chain_id_arbitrum.json");
6394 const CHAIN_ID_ETHEREUM: &str =
6395 include_str!("../../test_data/execution/rpc_eth_chain_id_ethereum.json");
6396 const GET_CODE_DEPLOYED: &str =
6397 include_str!("../../test_data/execution/rpc_eth_get_code_deployed.json");
6398 const GET_CODE_EMPTY: &str =
6399 include_str!("../../test_data/execution/rpc_eth_get_code_empty.json");
6400 const GET_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_get_balance.json");
6401 const GET_BALANCE_ZERO: &str =
6402 include_str!("../../test_data/execution/rpc_eth_get_balance_zero.json");
6403 const GET_BALANCE_INSUFFICIENT: &str =
6404 include_str!("../../test_data/execution/rpc_eth_get_balance_insufficient.json");
6405 const CALL_BALANCE: &str = include_str!("../../test_data/execution/rpc_eth_call_balance.json");
6406 const CALL_BALANCE_AFTER_WRAP: &str =
6407 include_str!("../../test_data/execution/rpc_eth_call_balance_after_wrap.json");
6408 const CALL_BALANCE_WETH: &str =
6409 include_str!("../../test_data/execution/rpc_eth_call_balance_weth.json");
6410 const CALL_BALANCE_USDC: &str =
6411 include_str!("../../test_data/execution/rpc_eth_call_balance_usdc.json");
6412 const CALL_BALANCE_WETH_UPDATED: &str =
6413 include_str!("../../test_data/execution/rpc_eth_call_balance_weth_updated.json");
6414 const CALL_BALANCE_USDC_UPDATED: &str =
6415 include_str!("../../test_data/execution/rpc_eth_call_balance_usdc_updated.json");
6416 const CALL_BOOL_TRUE: &str =
6417 include_str!("../../test_data/execution/rpc_eth_call_bool_true.json");
6418 const CALL_EMPTY: &str = include_str!("../../test_data/execution/rpc_eth_call_empty.json");
6419 const CALL_ZERO: &str = include_str!("../../test_data/execution/rpc_eth_call_zero.json");
6420 const CALL_ALLOWANCE: &str =
6421 include_str!("../../test_data/execution/rpc_eth_call_allowance.json");
6422 const CALL_ALLOWANCE_1000: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000003e8\"}";
6423 const CALL_ALLOWANCE_MAX: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff\"}";
6424 const CALL_FACTORY: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000001f98431c8ad98523631ae4a59f267346ea31f984\"}";
6425 const CALL_WETH: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000082af49447d8a07e3bd95bd0d56f35241523fbab1\"}";
6426 const CALL_USDC: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000af88d065e77c8cc2239327c5edb3a432268e5831\"}";
6427 const CALL_FEE_500: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x00000000000000000000000000000000000000000000000000000000000001f4\"}";
6428 const CALL_POOL: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x000000000000000000000000c6962004f452be9203591991d15f6b388e09e8d0\"}";
6429 const CALL_REVERTED: &str =
6430 r#"{"jsonrpc":"2.0","id":1,"error":{"code":3,"message":"execution reverted"}}"#;
6431 const STORAGE_ZERO: &str = r#"{"jsonrpc":"2.0","id":1,"result":"0x0000000000000000000000000000000000000000000000000000000000000000"}"#;
6432 const TRACE_UNKNOWN_TRANSACTION: &str =
6433 r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32000,"message":"transaction not found"}}"#;
6434 const CALL_DECIMALS_18: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000012\"}";
6435 const CALL_DECIMALS_6: &str = "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":\"0x0000000000000000000000000000000000000000000000000000000000000006\"}";
6436 const TRANSACTION_COUNT: &str =
6437 include_str!("../../test_data/execution/rpc_eth_get_transaction_count.json");
6438 const TRANSACTION_COUNT_NEXT: &str =
6439 include_str!("../../test_data/execution/rpc_eth_get_transaction_count_next.json");
6440 const ESTIMATE_GAS: &str = include_str!("../../test_data/execution/rpc_eth_estimate_gas.json");
6441 const MAX_PRIORITY_FEE: &str =
6442 include_str!("../../test_data/execution/rpc_eth_max_priority_fee_per_gas.json");
6443 const BLOCK_BY_NUMBER: &str =
6444 include_str!("../../test_data/execution/rpc_eth_get_block_by_number.json");
6445 const BLOCK_CANONICAL: &str =
6446 include_str!("../../test_data/execution/rpc_eth_get_block_canonical.json");
6447 const BLOCK_FINALIZED: &str =
6448 include_str!("../../test_data/execution/rpc_eth_get_block_finalized.json");
6449 const RECEIPT_SUCCESS: &str =
6450 include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_success.json");
6451 const RECEIPT_REVERTED: &str =
6452 include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_reverted.json");
6453 const RECEIPT_NULL: &str =
6454 include_str!("../../test_data/execution/rpc_eth_get_transaction_receipt_null.json");
6455 const SEND_RAW_TRANSACTION: &str =
6456 include_str!("../../test_data/execution/rpc_eth_send_raw_transaction.json");
6457 const SEND_RAW_TRANSACTION_REJECTED: &str =
6458 include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_rejected.json");
6459 const SEND_RAW_TRANSACTION_NONCE_TOO_LOW: &str =
6460 include_str!("../../test_data/execution/rpc_eth_send_raw_transaction_nonce_too_low.json");
6461 const RPC_METHOD_NOT_FOUND: &str =
6462 include_str!("../../test_data/execution/rpc_error_method_not_found.json");
6463
6464 const WALLET: &str = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266";
6465 const ROUTER: &str = "0xE592427A0AEce92De3Edee1F18E0157C05861564";
6466 const WETH: &str = "0x82aF49447D8a07e3bd95BD0d56f35241523fBab1";
6467 const USDC: &str = "0xaf88d065e77c8cC2239327C5EDb3A432268e5831";
6468 const TEST_TIMEOUT: Duration = Duration::from_secs(10);
6469
6470 const WETH_ADDRESS: Address = address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1");
6471 const USDC_ADDRESS: Address = address!("af88d065e77c8cC2239327C5EDb3A432268e5831");
6472 const ROUTER_ADDRESS: Address = address!("E592427A0AEce92De3Edee1F18E0157C05861564");
6473
6474 const TEST_PRIVATE_KEY: &str =
6476 "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80";
6477
6478 const BALANCE_OF_SELECTOR: &str = "0x70a08231";
6479 const ALLOWANCE_SELECTOR: &str = "0xdd62ed3e";
6480 const POOL_TOKEN0_SELECTOR: &str = "0x0dfe1681";
6481 const POOL_TOKEN1_SELECTOR: &str = "0xd21220a7";
6482 const POOL_FEE_SELECTOR: &str = "0xddca3f43";
6483 const DECIMALS_SELECTOR: &str = "0x313ce567";
6484 const FACTORY_SELECTOR: &str = "0xc45a0155";
6485 const WETH9_SELECTOR: &str = "0x4aa4a4fc";
6486 const GET_POOL_SELECTOR: &str = "0x1698ee82";
6487 const QUOTE_EXACT_INPUT_SELECTOR: &str = "0xc6a5026a";
6488 const QUOTE_EXACT_OUTPUT_SELECTOR: &str = "0xbd21704a";
6489
6490 fn test_pool() -> Pool {
6491 let chain = Arc::new(chains::ARBITRUM.clone());
6492 let dex = Arc::clone(&UNISWAP_V3.dex);
6493 let weth = Token::new(
6494 Arc::clone(&chain),
6495 address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
6496 "Wrapped Ether".to_string(),
6497 "WETH".to_string(),
6498 18,
6499 );
6500 let usdc = Token::new(
6501 Arc::clone(&chain),
6502 address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
6503 "USD Coin".to_string(),
6504 "USDC".to_string(),
6505 6,
6506 );
6507
6508 Pool::new(
6509 chain,
6510 dex,
6511 address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
6512 PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
6513 55_000_000,
6514 weth,
6515 usdc,
6516 Some(500),
6517 Some(10),
6518 UnixNanos::default(),
6519 )
6520 }
6521
6522 fn test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6523 test_config_with_signer_env(http_rpc_url, "BLOCKCHAIN_TEST_PRIVATE_KEY")
6524 }
6525
6526 fn test_config_with_signer_env(
6527 http_rpc_url: String,
6528 signer_env: &str,
6529 ) -> BlockchainExecutionClientConfig {
6530 let verifier_separator = if http_rpc_url.contains('?') { '&' } else { '?' };
6531 let code_hash = keccak256(
6532 hex::decode("6080604052348015600e575f5ffd5b5060").expect("valid test bytecode"),
6533 )
6534 .to_string();
6535 let result = |response: &str| {
6536 serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
6537 .as_str()
6538 .unwrap()
6539 .to_string()
6540 };
6541 let probe = |call_data: &str, expected_output: String| BlockchainContractProbe {
6542 call_data: call_data.to_string(),
6543 expected_output,
6544 };
6545 let contract = |address: &str, role| {
6546 let probes = match role {
6547 BlockchainContractRole::Router => vec![
6548 probe(FACTORY_SELECTOR, result(CALL_FACTORY)),
6549 probe(WETH9_SELECTOR, result(CALL_WETH)),
6550 ],
6551 BlockchainContractRole::Factory => vec![probe(
6552 &hex::encode_prefixed(
6553 UniswapV3Factory::getPoolCall {
6554 tokenA: WETH_ADDRESS,
6555 tokenB: USDC_ADDRESS,
6556 fee: U24::try_from(500u32).unwrap(),
6557 }
6558 .abi_encode(),
6559 ),
6560 result(CALL_POOL),
6561 )],
6562 BlockchainContractRole::WrappedNative => {
6563 vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_18))]
6564 }
6565 BlockchainContractRole::Quote => {
6566 vec![probe(FACTORY_SELECTOR, result(CALL_FACTORY))]
6567 }
6568 BlockchainContractRole::Token => {
6569 vec![probe(DECIMALS_SELECTOR, result(CALL_DECIMALS_6))]
6570 }
6571 BlockchainContractRole::Pool => vec![
6572 probe(POOL_TOKEN0_SELECTOR, result(CALL_WETH)),
6573 probe(POOL_TOKEN1_SELECTOR, result(CALL_USDC)),
6574 probe(POOL_FEE_SELECTOR, result(CALL_FEE_500)),
6575 ],
6576 BlockchainContractRole::Implementation => Vec::new(),
6577 };
6578 BlockchainContractManifest {
6579 address: address.to_string(),
6580 role,
6581 runtime_code_hash: code_hash.clone(),
6582 proxy: None,
6583 probes,
6584 }
6585 };
6586 let deployment_manifest = BlockchainDeploymentManifest {
6587 version: "test-v1".to_string(),
6588 chain_id: chains::ARBITRUM.chain_id,
6589 chain_name: chains::ARBITRUM.name.to_string(),
6590 contracts: vec![
6591 contract(ROUTER, BlockchainContractRole::Router),
6592 contract(
6593 "0x1F98431c8aD98523631AE4a59f267346ea31F984",
6594 BlockchainContractRole::Factory,
6595 ),
6596 contract(WETH, BlockchainContractRole::WrappedNative),
6597 contract(
6598 "0x61fFE014bA17989E743c5F6cB21bF9697530B21e",
6599 BlockchainContractRole::Quote,
6600 ),
6601 contract(USDC, BlockchainContractRole::Token),
6602 contract(
6603 "0xC6962004f452bE9203591991D15f6b388e09E8D0",
6604 BlockchainContractRole::Pool,
6605 ),
6606 ],
6607 tokens: vec![
6608 BlockchainTokenManifest {
6609 address: WETH.to_string(),
6610 name: "Wrapped Ether".to_string(),
6611 symbol: "WETH".to_string(),
6612 decimals: 18,
6613 asset_role: "both".to_string(),
6614 },
6615 BlockchainTokenManifest {
6616 address: USDC.to_string(),
6617 name: "USD Coin".to_string(),
6618 symbol: "USDC".to_string(),
6619 decimals: 6,
6620 asset_role: "both".to_string(),
6621 },
6622 ],
6623 pools: vec![BlockchainPoolManifest {
6624 address: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6625 token0: WETH.to_string(),
6626 token1: USDC.to_string(),
6627 fee: 500,
6628 factory: "0x1F98431c8aD98523631AE4a59f267346ea31F984".to_string(),
6629 quote_contract: "0x61fFE014bA17989E743c5F6cB21bF9697530B21e".to_string(),
6630 }],
6631 call_edges: ["swap_sell", "swap_buy"]
6632 .into_iter()
6633 .map(|purpose| BlockchainCallEdgeManifest {
6634 purpose: purpose.to_string(),
6635 caller: ROUTER.to_string(),
6636 target: "0xC6962004f452bE9203591991D15f6b388e09E8D0".to_string(),
6637 call_type: "call".to_string(),
6638 })
6639 .collect(),
6640 };
6641 let manifest_digest =
6642 keccak256(serde_json::to_vec(&deployment_manifest).unwrap()).to_string();
6643 let verification = BlockchainVerificationConfig {
6644 authoritative: BlockchainProviderIdentity {
6645 provider_id: "authoritative".to_string(),
6646 operator_id: "operator-a".to_string(),
6647 failure_domain_ids: vec!["domain-a".to_string()],
6648 },
6649 verifiers: vec![
6650 BlockchainVerificationProviderConfig {
6651 identity: BlockchainProviderIdentity {
6652 provider_id: "verifier-a".to_string(),
6653 operator_id: "operator-b".to_string(),
6654 failure_domain_ids: vec!["domain-b".to_string()],
6655 },
6656 http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-a")
6657 .into(),
6658 },
6659 BlockchainVerificationProviderConfig {
6660 identity: BlockchainProviderIdentity {
6661 provider_id: "verifier-b".to_string(),
6662 operator_id: "operator-c".to_string(),
6663 failure_domain_ids: vec!["domain-c".to_string()],
6664 },
6665 http_rpc_url: format!("{http_rpc_url}{verifier_separator}source=verifier-b")
6666 .into(),
6667 },
6668 ],
6669 chain_anchor: BlockchainChainAnchorConfig {
6670 chain_id: chains::ARBITRUM.chain_id,
6671 chain_name: chains::ARBITRUM.name.to_string(),
6672 checkpoint_height: 30_346_560,
6673 checkpoint_hash:
6674 "0x1111111111111111111111111111111111111111111111111111111111111111".to_string(),
6675 checkpoint_timestamp: 1_761_888_800,
6676 max_head_skew_blocks: 3,
6677 max_head_age_secs: u64::MAX,
6678 max_future_drift_secs: u64::MAX,
6679 },
6680 manifest_version: "test-v1".to_string(),
6681 manifest_digest,
6682 deployment_manifest,
6683 };
6684 BlockchainExecutionClientConfig::builder()
6685 .client_id(AccountId::from("BLOCKCHAIN-001"))
6686 .chain(chains::ARBITRUM.clone())
6687 .wallet_address(WALLET.to_string())
6688 .http_rpc_url(http_rpc_url.into())
6689 .verification(verification)
6690 .signer_private_key_env(signer_env.to_string())
6691 .router_addresses(vec![ROUTER.to_string()])
6692 .weth_address(WETH.to_string())
6693 .max_fee_per_gas_wei(1_000_000_000)
6694 .base_fee_buffer_bps(2_000)
6695 .gas_limit(1_000_000)
6696 .gas_buffer_bps(2_000)
6697 .allowed_token_pairs(vec![(WETH.to_string(), USDC.to_string())])
6698 .slippage_bps(50)
6699 .max_slippage_bps(200)
6700 .max_order_amount(1_000_000_000_000_000_000)
6701 .deadline_seconds(300)
6702 .max_quote_age_blocks(100)
6703 .receipt_timeout_secs(1)
6704 .build()
6705 }
6706
6707 fn buy_test_config(http_rpc_url: String) -> BlockchainExecutionClientConfig {
6708 let max_amount = expected_buy_amount_in().to_string();
6709 let mut config = test_config(http_rpc_url);
6710 config.allowed_token_pairs = Some(vec![
6711 (WETH.to_string(), USDC.to_string()),
6712 (USDC.to_string(), WETH.to_string()),
6713 ]);
6714 config.quote_spend_limits = Some(vec![quote_spend_limit(USDC, WETH, 6, &max_amount)]);
6715 config
6716 }
6717
6718 fn refresh_test_manifest_digest(config: &mut BlockchainExecutionClientConfig) {
6719 let verification = config.verification.as_mut().unwrap();
6720 verification.manifest_digest =
6721 keccak256(serde_json::to_vec(&verification.deployment_manifest).unwrap()).to_string();
6722 }
6723
6724 fn quote_spend_limit(
6725 token_in: &str,
6726 token_out: &str,
6727 spend_token_decimals: u8,
6728 max_amount: &str,
6729 ) -> QuoteSpendLimit {
6730 QuoteSpendLimit::builder()
6731 .token_in(token_in.to_string())
6732 .token_out(token_out.to_string())
6733 .spend_token(token_in.to_string())
6734 .spend_token_decimals(spend_token_decimals)
6735 .max_amount(max_amount.to_string())
6736 .build()
6737 }
6738
6739 fn test_client_from_config(
6740 config: BlockchainExecutionClientConfig,
6741 pool: Pool,
6742 ) -> BlockchainExecutionClient {
6743 test_client_result(config, pool).unwrap()
6744 }
6745
6746 fn test_client_result(
6747 config: BlockchainExecutionClientConfig,
6748 pool: Pool,
6749 ) -> anyhow::Result<BlockchainExecutionClient> {
6750 test_client_and_cache(config, pool).map(|(client, _)| client)
6751 }
6752
6753 fn test_client_and_cache(
6754 config: BlockchainExecutionClientConfig,
6755 pool: Pool,
6756 ) -> anyhow::Result<(BlockchainExecutionClient, Rc<RefCell<Cache>>)> {
6757 let cache = Rc::new(RefCell::new(Cache::default()));
6758 cache.borrow_mut().add_pool(pool).unwrap();
6759 let core = ExecutionClientCore::new(
6760 TraderId::from("TRADER-001"),
6761 ClientId::from("BLOCKCHAIN-001"),
6762 *BLOCKCHAIN_VENUE,
6763 OmsType::Netting,
6764 AccountId::from("BLOCKCHAIN-001"),
6765 AccountType::Wallet,
6766 None,
6767 Rc::clone(&cache),
6768 );
6769
6770 let client = BlockchainExecutionClient::new(core, config)?;
6771 Ok((client, cache))
6772 }
6773
6774 fn test_client(http_rpc_url: String) -> BlockchainExecutionClient {
6775 test_client_from_config(test_config(http_rpc_url), test_pool())
6776 }
6777
6778 async fn client_with_mock_rpc(
6779 state: MockRpcState,
6780 ) -> (BlockchainExecutionClient, MockRpcState) {
6781 let addr = start_mock_rpc_server(state.clone()).await;
6782 (test_client(format!("http://{addr}")), state)
6783 }
6784
6785 async fn client_with_token_mock_rpc(
6786 state: MockRpcState,
6787 signer_env: &str,
6788 ) -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
6789 let state = with_connect_capabilities(state);
6790 let addr = start_mock_rpc_server(state.clone()).await;
6791 let pool = test_pool();
6792 let tokens = [pool.token0.clone(), pool.token1.clone()];
6793 let mut config = test_config_with_signer_env(format!("http://{addr}"), signer_env);
6794 config.tokens = Some(vec![WETH.to_string(), USDC.to_string()]);
6795 let (mut client, cache) = test_client_and_cache(config, pool).unwrap();
6796 for token in tokens {
6797 client.cache.add_token(token).await.unwrap();
6798 }
6799 (client, state, cache)
6800 }
6801
6802 fn execution_rpc_state() -> MockRpcState {
6803 MockRpcState::default()
6804 .with_receipt_hash_from_request()
6805 .with_response("eth_chainId", CHAIN_ID_ARBITRUM)
6806 .with_response("eth_getCode", GET_CODE_DEPLOYED)
6807 .with_response("eth_getBalance", GET_BALANCE)
6808 .with_response("eth_getBlockByNumber", BLOCK_BY_NUMBER)
6809 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6810 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", BLOCK_CANONICAL)
6811 .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_FINALIZED)
6812 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d42", BLOCK_FINALIZED)
6813 .with_response("eth_maxPriorityFeePerGas", MAX_PRIORITY_FEE)
6814 .with_call_response(FACTORY_SELECTOR, CALL_FACTORY)
6815 .with_call_response(WETH9_SELECTOR, CALL_WETH)
6816 .with_call_response(GET_POOL_SELECTOR, CALL_POOL)
6817 .with_call_response(POOL_TOKEN0_SELECTOR, CALL_WETH)
6818 .with_call_response(POOL_TOKEN1_SELECTOR, CALL_USDC)
6819 .with_call_response(POOL_FEE_SELECTOR, CALL_FEE_500)
6820 .with_contract_call_response(WETH, DECIMALS_SELECTOR, CALL_DECIMALS_18)
6821 .with_contract_call_response(USDC, DECIMALS_SELECTOR, CALL_DECIMALS_6)
6822 .with_call_response(
6823 QUOTE_EXACT_INPUT_SELECTOR,
6824 "e_response(expected_sell_quote_amount()),
6825 )
6826 .with_call_response(
6827 QUOTE_EXACT_OUTPUT_SELECTOR,
6828 "e_response(expected_buy_amount_in()),
6829 )
6830 }
6831
6832 fn ready_rpc_state() -> MockRpcState {
6833 with_connect_capabilities(execution_rpc_state())
6834 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
6835 .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE)
6836 }
6837
6838 fn with_connect_capabilities(state: MockRpcState) -> MockRpcState {
6839 state
6840 .with_response("eth_getStorageAt", STORAGE_ZERO)
6841 .with_response("eth_estimateGas", ESTIMATE_GAS)
6842 .with_parameter_response(
6843 "debug_traceTransaction",
6844 &B256::ZERO.to_string(),
6845 TRACE_UNKNOWN_TRANSACTION,
6846 )
6847 }
6848
6849 fn signing_rpc_state() -> MockRpcState {
6850 ready_rpc_state()
6851 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6852 .with_response("eth_estimateGas", ESTIMATE_GAS)
6853 }
6854
6855 fn broadcast_rpc_state() -> MockRpcState {
6856 execution_rpc_state()
6857 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
6858 .with_response("eth_estimateGas", ESTIMATE_GAS)
6859 .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
6860 .with_send_raw_transaction_echo()
6861 .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO, CALL_ALLOWANCE_1000])
6862 }
6863
6864 async fn expected_wrap_tx_hash(value: U256) -> B256 {
6865 expected_tx_hash(
6866 WETH_ADDRESS,
6867 value,
6868 Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
6869 )
6870 .await
6871 }
6872
6873 async fn expected_approve_tx_hash(amount: U256) -> B256 {
6874 let calldata = ERC20::approveCall {
6875 spender: ROUTER_ADDRESS,
6876 amount,
6877 }
6878 .abi_encode();
6879 expected_tx_hash(WETH_ADDRESS, U256::ZERO, Bytes::from(calldata)).await
6880 }
6881
6882 async fn expected_tx_hash(to: Address, value: U256, input: Bytes) -> B256 {
6883 let expected_tx =
6886 build_eip1559_transaction(42161, 7, 78_000, 130_000_000, 10_000_000, to, value, input);
6887 let (expected_hash, _) = sign_eip1559_transaction(
6888 expected_tx,
6889 &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
6890 )
6891 .await
6892 .unwrap();
6893 expected_hash
6894 }
6895
6896 async fn await_recorded_requests(state: &MockRpcState, method: &str, expected: usize) {
6897 wait_until_async(
6898 || async {
6899 state
6900 .recorded_requests()
6901 .iter()
6902 .filter(|request| request["method"] == method)
6903 .count()
6904 >= expected
6905 },
6906 TEST_TIMEOUT,
6907 )
6908 .await;
6909 }
6910
6911 const FIXTURE_BLOCK: u64 = 30_346_560;
6914 const FIXTURE_BLOCK_PARAM: &str = "0x1cf0d40";
6915 const FIXTURE_BLOCK_HASH: &str =
6916 "0x1111111111111111111111111111111111111111111111111111111111111111";
6917 const FIXTURE_BLOCK_TIMESTAMP: u64 = 1_761_888_800;
6919 const TEST_LIQUIDITY: u128 = 1_000_000_000_000_000_000_000;
6921
6922 fn test_profiler(pool: &Pool, block_number: u64) -> PoolProfiler {
6923 test_profiler_at_block(pool, block_number, FIXTURE_BLOCK_HASH)
6924 }
6925
6926 fn test_profiler_at_block(pool: &Pool, block_number: u64, block_hash: &str) -> PoolProfiler {
6927 test_profiler_with_range(
6928 pool,
6929 block_number,
6930 block_hash,
6931 U160::from(1u128 << 96),
6932 -887_220,
6933 887_220,
6934 TEST_LIQUIDITY,
6935 )
6936 }
6937
6938 fn test_profiler_with_state(
6939 pool: &Pool,
6940 block_number: u64,
6941 sqrt_price_x96: U160,
6942 liquidity: u128,
6943 ) -> PoolProfiler {
6944 test_profiler_with_range(
6945 pool,
6946 block_number,
6947 FIXTURE_BLOCK_HASH,
6948 sqrt_price_x96,
6949 -887_220,
6950 887_220,
6951 liquidity,
6952 )
6953 }
6954
6955 fn test_profiler_with_range(
6959 pool: &Pool,
6960 block_number: u64,
6961 block_hash: &str,
6962 sqrt_price_x96: U160,
6963 tick_lower: i32,
6964 tick_upper: i32,
6965 liquidity: u128,
6966 ) -> PoolProfiler {
6967 let snapshot = PoolSnapshot::new(
6968 pool.instrument_id,
6969 PoolState {
6970 current_tick: get_tick_at_sqrt_ratio(sqrt_price_x96),
6971 price_sqrt_ratio_x96: sqrt_price_x96,
6972 liquidity,
6973 protocol_fees_token0: U256::ZERO,
6974 protocol_fees_token1: U256::ZERO,
6975 fee_protocol: 0,
6976 fee_protocol0_basis_points: None,
6977 fee_protocol1_basis_points: None,
6978 fee_growth_global_0: U256::ZERO,
6979 fee_growth_global_1: U256::ZERO,
6980 },
6981 vec![PoolPosition::new(
6982 address!("DeaDbeefdEAdbeefdEadbEEFdeadbeEFdEaDbeeF"),
6983 tick_lower,
6984 tick_upper,
6985 liquidity as i128,
6986 )],
6987 vec![
6988 PoolTick::new(
6989 tick_lower,
6990 liquidity,
6991 liquidity as i128,
6992 U256::ZERO,
6993 U256::ZERO,
6994 true,
6995 0,
6996 ),
6997 PoolTick::new(
6998 tick_upper,
6999 liquidity,
7000 -(liquidity as i128),
7001 U256::ZERO,
7002 U256::ZERO,
7003 true,
7004 0,
7005 ),
7006 ],
7007 PoolAnalytics::default(),
7008 BlockPosition::new(
7009 block_number,
7010 block_hash.to_string(),
7011 BLOCK_SCOPED_SNAPSHOT_INDEX,
7012 BLOCK_SCOPED_SNAPSHOT_INDEX,
7013 )
7014 .with_block_hash(Some(block_hash.to_string())),
7015 UnixNanos::default(),
7016 UnixNanos::default(),
7017 );
7018 let mut profiler = PoolProfiler::new(Arc::new(pool.clone()));
7019 profiler.restore_from_snapshot(snapshot).unwrap();
7020 profiler
7021 }
7022
7023 fn test_market_sell_order(instrument_id: InstrumentId) -> OrderAny {
7024 market_sell_order_with_id(instrument_id, "O-SWAP-001")
7025 }
7026
7027 fn test_market_buy_order(instrument_id: InstrumentId) -> OrderAny {
7028 market_buy_order_with_id(instrument_id, "O-SWAP-BUY-001")
7029 }
7030
7031 fn submit_order_cmd(order: &OrderAny) -> SubmitOrder {
7032 SubmitOrder::new(
7033 TraderId::from("TRADER-001"),
7034 Some(ClientId::from("BLOCKCHAIN-001")),
7035 order.strategy_id(),
7036 order.instrument_id(),
7037 order.client_order_id(),
7038 order.init_event().clone(),
7039 None,
7040 None,
7041 None,
7042 UUID4::new(),
7043 UnixNanos::default(),
7044 None,
7045 )
7046 }
7047
7048 fn swap_client_with_cache(
7051 config: BlockchainExecutionClientConfig,
7052 ) -> (BlockchainExecutionClient, Rc<RefCell<Cache>>) {
7053 let cache = Rc::new(RefCell::new(Cache::default()));
7054 let pool = test_pool();
7055 cache.borrow_mut().add_pool(pool.clone()).unwrap();
7056 cache
7057 .borrow_mut()
7058 .add_order(
7059 test_market_sell_order(pool.instrument_id),
7060 None,
7061 None,
7062 false,
7063 )
7064 .unwrap();
7065 cache
7066 .borrow_mut()
7067 .add_pool_profiler(test_profiler_at_block(
7068 &pool,
7069 FIXTURE_BLOCK,
7070 FIXTURE_BLOCK_HASH,
7071 ))
7072 .unwrap();
7073 let core = ExecutionClientCore::new(
7074 TraderId::from("TRADER-001"),
7075 ClientId::from("BLOCKCHAIN-001"),
7076 *BLOCKCHAIN_VENUE,
7077 OmsType::Netting,
7078 AccountId::from("BLOCKCHAIN-001"),
7079 AccountType::Wallet,
7080 None,
7081 Rc::clone(&cache),
7082 );
7083
7084 let client = BlockchainExecutionClient::new(core, config).unwrap();
7085 (client, cache)
7086 }
7087
7088 async fn swap_client_with_database(
7090 test_name: &str,
7091 state: MockRpcState,
7092 ) -> Option<(
7093 sqlx::PgPool,
7094 String,
7095 BlockchainExecutionClient,
7096 MockRpcState,
7097 Rc<RefCell<Cache>>,
7098 )> {
7099 swap_client_with_database_config(test_name, state, test_config).await
7100 }
7101
7102 async fn swap_client_with_buy_database(
7103 test_name: &str,
7104 state: MockRpcState,
7105 ) -> Option<(
7106 sqlx::PgPool,
7107 String,
7108 BlockchainExecutionClient,
7109 MockRpcState,
7110 Rc<RefCell<Cache>>,
7111 )> {
7112 swap_client_with_database_config(test_name, state, buy_test_config).await
7113 }
7114
7115 async fn swap_client_with_database_config<F>(
7116 test_name: &str,
7117 state: MockRpcState,
7118 config: F,
7119 ) -> Option<(
7120 sqlx::PgPool,
7121 String,
7122 BlockchainExecutionClient,
7123 MockRpcState,
7124 Rc<RefCell<Cache>>,
7125 )>
7126 where
7127 F: FnOnce(String) -> BlockchainExecutionClientConfig,
7128 {
7129 let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
7130 let schema = format!("{test_name}_{}", std::process::id());
7131 setup_execution_schema(&admin_pool, &schema).await;
7132
7133 let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
7134 let db_options = db_options.options([("search_path", schema.clone())]);
7135 let database = connect_test_database(db_options).await.unwrap();
7136 let addr = start_mock_rpc_server(state.clone()).await;
7137 let (mut client, cache) = swap_client_with_cache(config(format!("http://{addr}")));
7138 client.cache.database = Some(database);
7139 client
7141 .cache
7142 .ensure_execution_transaction_schema()
7143 .await
7144 .unwrap();
7145 protect_test_storage(&mut client, &schema).await;
7146 initialize_test_verification_ledger(&client).await;
7147 client.signer = Some(Arc::new(
7148 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7149 ));
7150 client.core.set_connected();
7151
7152 Some((admin_pool, schema, client, state, cache))
7153 }
7154
7155 async fn initialize_test_verification_ledger(client: &BlockchainExecutionClient) {
7156 initialize_test_verification_ledger_with_headers(
7157 client,
7158 &[ExecutionVerifiedHeader {
7159 number: FIXTURE_BLOCK,
7160 hash: FIXTURE_BLOCK_HASH.to_string(),
7161 parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
7162 .to_string(),
7163 timestamp: FIXTURE_BLOCK_TIMESTAMP,
7164 base_fee_per_gas: Some(100_000_000),
7165 }],
7166 )
7167 .await;
7168 }
7169
7170 async fn initialize_test_verification_ledger_with_headers(
7171 client: &BlockchainExecutionClient,
7172 finalized_headers: &[ExecutionVerifiedHeader],
7173 ) {
7174 ensure_test_verification_ledger(client, finalized_headers, 7, 7)
7175 .await
7176 .unwrap();
7177 }
7178
7179 async fn ensure_test_verification_ledger(
7180 client: &BlockchainExecutionClient,
7181 finalized_headers: &[ExecutionVerifiedHeader],
7182 next_canonical_nonce: u64,
7183 observed_canonical_nonce: u64,
7184 ) -> anyhow::Result<()> {
7185 let verification = client.config.verification.as_ref().unwrap();
7186 let provider_ids = vec![
7187 "authoritative".to_string(),
7188 "verifier-a".to_string(),
7189 "verifier-b".to_string(),
7190 ];
7191 let operator_ids = vec![
7192 "operator-a".to_string(),
7193 "operator-b".to_string(),
7194 "operator-c".to_string(),
7195 ];
7196 let failure_domain_ids = vec![
7197 "domain-a".to_string(),
7198 "domain-b".to_string(),
7199 "domain-c".to_string(),
7200 ];
7201 let decisions = [ExecutionVerificationDecision {
7202 read_class: "numbered_block",
7203 height_start: Some(FIXTURE_BLOCK),
7204 height_end: Some(FIXTURE_BLOCK),
7205 normalized_value_digest: B256::ZERO.to_string(),
7206 }];
7207 client
7208 .cache
7209 .database
7210 .as_ref()
7211 .unwrap()
7212 .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7213 chain_id: 42_161,
7214 wallet_address: WALLET,
7215 manifest_version: &verification.manifest_version,
7216 manifest_digest: &verification.manifest_digest,
7217 checkpoint_number: FIXTURE_BLOCK,
7218 checkpoint_hash: FIXTURE_BLOCK_HASH,
7219 checkpoint_parent_hash:
7220 "0x0000000000000000000000000000000000000000000000000000000000000001",
7221 checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7222 checkpoint_base_fee_per_gas: Some(100_000_000),
7223 finalized_headers,
7224 next_canonical_nonce,
7225 observed_canonical_nonce,
7226 provider_ids: &provider_ids,
7227 operator_ids: &operator_ids,
7228 failure_domain_ids: &failure_domain_ids,
7229 decisions: &decisions,
7230 migration: None,
7231 })
7232 .await
7233 }
7234
7235 async fn initialize_test_verification_migration(
7236 client: &BlockchainExecutionClient,
7237 finalized_headers: &[ExecutionVerifiedHeader],
7238 next_canonical_nonce: u64,
7239 decisions: &[ExecutionVerificationDecision],
7240 migration: &ExecutionVerificationMigration,
7241 ) {
7242 let verification = client.config.verification.as_ref().unwrap();
7243 let provider_ids = vec![
7244 "authoritative".to_string(),
7245 "verifier-a".to_string(),
7246 "verifier-b".to_string(),
7247 ];
7248 let operator_ids = vec![
7249 "operator-a".to_string(),
7250 "operator-b".to_string(),
7251 "operator-c".to_string(),
7252 ];
7253 let failure_domain_ids = vec![
7254 "domain-a".to_string(),
7255 "domain-b".to_string(),
7256 "domain-c".to_string(),
7257 ];
7258 client
7259 .cache
7260 .database
7261 .as_ref()
7262 .unwrap()
7263 .ensure_execution_verification_schema(&ExecutionVerificationBootstrap {
7264 chain_id: 42_161,
7265 wallet_address: WALLET,
7266 manifest_version: &verification.manifest_version,
7267 manifest_digest: &verification.manifest_digest,
7268 checkpoint_number: FIXTURE_BLOCK,
7269 checkpoint_hash: FIXTURE_BLOCK_HASH,
7270 checkpoint_parent_hash:
7271 "0x0000000000000000000000000000000000000000000000000000000000000001",
7272 checkpoint_timestamp: FIXTURE_BLOCK_TIMESTAMP,
7273 checkpoint_base_fee_per_gas: Some(100_000_000),
7274 finalized_headers,
7275 next_canonical_nonce,
7276 observed_canonical_nonce: next_canonical_nonce,
7277 provider_ids: &provider_ids,
7278 operator_ids: &operator_ids,
7279 failure_domain_ids: &failure_domain_ids,
7280 decisions,
7281 migration: Some(migration),
7282 })
7283 .await
7284 .unwrap();
7285 }
7286
7287 async fn swap_rpc_state() -> MockRpcState {
7288 let min_amount_out = expected_min_amount_out(50);
7289 swap_rpc_state_with_min_amount_out(min_amount_out).await
7290 }
7291
7292 async fn swap_rpc_state_with_min_amount_out(min_amount_out: U256) -> MockRpcState {
7293 let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
7294 finalized_swap_rpc_state(tx_hash, min_amount_out)
7295 }
7296
7297 async fn swap_rpc_state_for_mismatch() -> MockRpcState {
7298 swap_rpc_state()
7299 .await
7300 .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION)
7301 }
7302
7303 fn awaiting_in_flight(client: &BlockchainExecutionClient) -> InFlightTransaction {
7304 let slot = *client.in_flight.lock();
7305 let Some(InFlightSlot::AwaitingFinality(in_flight)) = slot else {
7306 panic!("expected an awaiting-finality transaction, was {slot:?}");
7307 };
7308 in_flight
7309 }
7310
7311 fn recovering_in_flight(client: &BlockchainExecutionClient) -> RecoveryTransaction {
7312 let slot = *client.in_flight.lock();
7313 let Some(InFlightSlot::Recovering(recovery)) = slot else {
7314 panic!("expected a recovery transaction, was {slot:?}");
7315 };
7316 recovery
7317 }
7318
7319 async fn execution_intent_markers(
7320 admin_pool: &sqlx::PgPool,
7321 schema: &str,
7322 ) -> Vec<(String, String, bool, bool)> {
7323 sqlx::query_as(sqlx::AssertSqlSafe(format!(
7324 "SELECT purpose, status, terminal_emitted, active \
7325 FROM {schema}.execution_intent ORDER BY id"
7326 )))
7327 .fetch_all(admin_pool)
7328 .await
7329 .unwrap()
7330 }
7331
7332 #[allow(unsafe_code)] fn payload_test_keys(
7334 active: [u8; 32],
7335 retired: Vec<[u8; 32]>,
7336 deployment_id: &str,
7337 ) -> PayloadKeySet {
7338 static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7339
7340 let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7341 let active_env = format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_ACTIVE");
7342 let retired_envs = retired
7343 .iter()
7344 .enumerate()
7345 .map(|(index, _)| format!("BLOCKCHAIN_TEST_PAYLOAD_KEY_{env_id}_RETIRED_{index}"))
7346 .collect::<Vec<_>>();
7347 unsafe { std::env::set_var(&active_env, hex::encode(active)) };
7349 for (env, key) in retired_envs.iter().zip(&retired) {
7350 unsafe { std::env::set_var(env, hex::encode(key)) };
7352 }
7353
7354 let keys = PayloadKeySet::load(Some(&active_env), &retired_envs, Some(deployment_id))
7355 .unwrap()
7356 .unwrap();
7357
7358 unsafe { std::env::remove_var(active_env) };
7360 for env in retired_envs {
7361 unsafe { std::env::remove_var(env) };
7363 }
7364 keys
7365 }
7366
7367 #[allow(unsafe_code)] fn set_test_payload_key(
7369 client: &mut BlockchainExecutionClient,
7370 key: [u8; 32],
7371 deployment_id: &str,
7372 ) -> PayloadKeySet {
7373 static NEXT_ENV_ID: AtomicU64 = AtomicU64::new(0);
7374
7375 let env_id = NEXT_ENV_ID.fetch_add(1, Ordering::Relaxed);
7376 let active_env = format!("BLOCKCHAIN_TEST_EXECUTION_KEY_{env_id}");
7377 unsafe { std::env::set_var(&active_env, hex::encode(key)) };
7379 client.config.payload_key_env = Some(active_env);
7380 client.config.payload_deployment_id = Some(deployment_id.to_string());
7381 client.load_payload_keys().unwrap().unwrap()
7382 }
7383
7384 async fn protect_test_storage(client: &mut BlockchainExecutionClient, deployment_id: &str) {
7385 let keys = set_test_payload_key(client, [0xa5; 32], deployment_id);
7386 client
7387 .cache
7388 .database
7389 .as_ref()
7390 .unwrap()
7391 .ensure_execution_payload_storage(&keys)
7392 .await
7393 .unwrap();
7394 client.payload_keys = Some(Arc::new(keys));
7395 }
7396
7397 async fn reserve_test_wrap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7398 reserve_test_wrap_intent_for_wallet(database, WALLET).await
7399 }
7400
7401 async fn reserve_test_wrap_intent_for_wallet(
7402 database: &BlockchainCacheDatabase,
7403 wallet: &str,
7404 ) -> ExecutionIntentRow {
7405 database
7406 .reserve_execution_intent(&ExecutionIntentInsert {
7407 chain_id: 42161,
7408 wallet_address: wallet.to_string(),
7409 purpose: "wrap".to_string(),
7410 client_order_id: None,
7411 trader_id: None,
7412 strategy_id: None,
7413 account_id: None,
7414 instrument_id: None,
7415 pool_address: None,
7416 transaction_to: WETH_ADDRESS.to_string(),
7417 transaction_input: "0xd0e30db0".to_string(),
7418 transaction_value: "1".to_string(),
7419 amount_in: None,
7420 created_block: FIXTURE_BLOCK,
7421 })
7422 .await
7423 .unwrap()
7424 }
7425
7426 async fn persist_test_wrap_broadcast(
7427 database: &BlockchainCacheDatabase,
7428 keys: Option<&PayloadKeySet>,
7429 ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7430 let intent = reserve_test_wrap_intent(database).await;
7431 database
7432 .assign_execution_intent_nonce(intent.id, 7)
7433 .await
7434 .unwrap();
7435 let intent = database.get_execution_intent(intent.id).await.unwrap();
7436 let transaction = build_eip1559_transaction(
7437 42161,
7438 7,
7439 78_000,
7440 130_000_000,
7441 10_000_000,
7442 WETH_ADDRESS,
7443 U256::from(1u64),
7444 Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
7445 );
7446 let (tx_hash, raw_tx) = sign_eip1559_transaction(
7447 transaction,
7448 &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7449 )
7450 .await
7451 .unwrap();
7452 persist_test_payload(database, keys, &intent, tx_hash, &raw_tx).await;
7453 database
7454 .record_execution_status(
7455 intent.id,
7456 &tx_hash.to_string(),
7457 TransactionStatus::Broadcast,
7458 None,
7459 None,
7460 None,
7461 None,
7462 None,
7463 )
7464 .await
7465 .unwrap();
7466 (intent, tx_hash, raw_tx)
7467 }
7468
7469 async fn reserve_test_swap_intent(database: &BlockchainCacheDatabase) -> ExecutionIntentRow {
7470 let pool = test_pool();
7471 let order = test_market_sell_order(pool.instrument_id);
7472 let calldata = expected_swap_calldata(expected_min_amount_out(50));
7473
7474 database
7475 .reserve_execution_intent(&ExecutionIntentInsert {
7476 chain_id: 42161,
7477 wallet_address: WALLET.to_string(),
7478 purpose: "swap".to_string(),
7479 client_order_id: Some(order.client_order_id().to_string()),
7480 trader_id: Some(order.trader_id().to_string()),
7481 strategy_id: Some(order.strategy_id().to_string()),
7482 account_id: Some("BLOCKCHAIN-001".to_string()),
7483 instrument_id: Some(pool.instrument_id.to_string()),
7484 pool_address: Some(pool.address.to_string()),
7485 transaction_to: ROUTER_ADDRESS.to_string(),
7486 transaction_input: hex::encode_prefixed(&calldata),
7487 transaction_value: U256::ZERO.to_string(),
7488 amount_in: Some("1000000000000000".to_string()),
7489 created_block: FIXTURE_BLOCK,
7490 })
7491 .await
7492 .unwrap()
7493 }
7494
7495 async fn persist_invalid_test_swap(
7496 database: &BlockchainCacheDatabase,
7497 keys: Option<&PayloadKeySet>,
7498 ) -> (ExecutionIntentRow, B256) {
7499 let intent = reserve_test_swap_intent(database).await;
7500 database
7501 .assign_execution_intent_nonce(intent.id, 7)
7502 .await
7503 .unwrap();
7504 let intent = database.get_execution_intent(intent.id).await.unwrap();
7505 let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7506 let mut raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7507 raw_transaction.push(0xff);
7508 persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7509 database
7510 .record_execution_status(
7511 intent.id,
7512 &tx_hash.to_string(),
7513 TransactionStatus::Broadcast,
7514 None,
7515 None,
7516 None,
7517 None,
7518 None,
7519 )
7520 .await
7521 .unwrap();
7522
7523 (intent, tx_hash)
7524 }
7525
7526 async fn persist_test_swap_broadcast(
7527 database: &BlockchainCacheDatabase,
7528 keys: Option<&PayloadKeySet>,
7529 ) -> (ExecutionIntentRow, B256, Vec<u8>) {
7530 let intent = reserve_test_swap_intent(database).await;
7531 database
7532 .assign_execution_intent_nonce(intent.id, 7)
7533 .await
7534 .unwrap();
7535 let intent = database.get_execution_intent(intent.id).await.unwrap();
7536 let (tx_hash, raw_transaction) = expected_swap_tx(expected_min_amount_out(50)).await;
7537 let raw_transaction = hex::decode(raw_transaction.strip_prefix("0x").unwrap()).unwrap();
7538 persist_test_payload(database, keys, &intent, tx_hash, &raw_transaction).await;
7539 database
7540 .record_execution_status(
7541 intent.id,
7542 &tx_hash.to_string(),
7543 TransactionStatus::Broadcast,
7544 None,
7545 None,
7546 None,
7547 None,
7548 None,
7549 )
7550 .await
7551 .unwrap();
7552 (intent, tx_hash, raw_transaction)
7553 }
7554
7555 async fn persist_test_payload(
7556 database: &BlockchainCacheDatabase,
7557 keys: Option<&PayloadKeySet>,
7558 intent: &ExecutionIntentRow,
7559 tx_hash: B256,
7560 raw_transaction: &[u8],
7561 ) {
7562 if let Some(keys) = keys {
7563 database.reserve_execution_payload_seal(keys).await.unwrap();
7564 let transaction_hash = tx_hash.to_string();
7565 let context =
7566 payload_context_identity(intent, &transaction_hash, 42_161, keys.deployment_id())
7567 .unwrap();
7568 let envelope = keys.seal(raw_transaction, &context).unwrap();
7569 database
7570 .add_execution_transaction_envelope(intent.id, 42_161, &transaction_hash, &envelope)
7571 .await
7572 .unwrap();
7573 } else {
7574 database
7575 .add_execution_transaction_hash(
7576 intent.id,
7577 42_161,
7578 &tx_hash.to_string(),
7579 raw_transaction,
7580 )
7581 .await
7582 .unwrap();
7583 }
7584 }
7585
7586 async fn later_reconnect(
7587 previous: BlockchainExecutionClient,
7588 http_rpc_url: String,
7589 ) -> anyhow::Error {
7590 let database = previous.cache.database.as_ref().unwrap().clone();
7591 let payload_keys = previous.payload_keys.clone();
7592 drop(previous);
7593 let mut next = test_client(http_rpc_url);
7594 next.cache.database = Some(database);
7595 next.payload_keys = payload_keys;
7596 next.signer = Some(Arc::new(
7597 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7598 ));
7599 next.reconcile_unresolved_execution().await.unwrap_err()
7600 }
7601
7602 fn start_with_events(
7603 client: &mut BlockchainExecutionClient,
7604 ) -> tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent> {
7605 let (sender, receiver) = tokio::sync::mpsc::unbounded_channel();
7606 replace_exec_event_sender(sender);
7607 client.start().unwrap();
7608 receiver
7609 }
7610
7611 async fn await_pending_tasks(client: &BlockchainExecutionClient) {
7612 tokio::time::timeout(TEST_TIMEOUT, async {
7613 while !client.pending_tasks.all_finished() {
7614 tokio::time::sleep(Duration::from_millis(1)).await;
7615 }
7616 })
7617 .await
7618 .unwrap();
7619 }
7620
7621 fn collect_order_events(
7622 receiver: &mut tokio::sync::mpsc::UnboundedReceiver<ExecutionEvent>,
7623 ) -> Vec<OrderEventAny> {
7624 let mut events = Vec::new();
7625
7626 while let Ok(event) = receiver.try_recv() {
7627 if let ExecutionEvent::Order(order_event) = event {
7628 events.push(order_event);
7629 }
7630 }
7631 events
7632 }
7633
7634 fn assert_swap_quarantined_without_terminal_event(events: &[OrderEventAny]) {
7635 assert_eq!(events.len(), 1, "was: {events:?}");
7636 assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7637 }
7638
7639 fn assert_swap_submitted_and_filled(events: &[OrderEventAny]) {
7640 assert_eq!(events.len(), 2, "was: {events:?}");
7641 assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
7642 assert!(matches!(&events[1], OrderEventAny::Filled(_)));
7643 }
7644
7645 fn expected_swap_calldata(min_amount_out: U256) -> Vec<u8> {
7646 UniswapV3SwapRouter::exactInputSingleCall {
7647 params: UniswapV3SwapRouter::ExactInputSingleParams {
7648 tokenIn: WETH_ADDRESS,
7649 tokenOut: address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
7650 fee: U24::try_from(500u32).unwrap(),
7651 recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7652 deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7653 amountIn: U256::from(1_000_000_000_000_000u64),
7654 amountOutMinimum: min_amount_out,
7655 sqrtPriceLimitX96: U160::ZERO,
7656 },
7657 }
7658 .abi_encode()
7659 }
7660
7661 fn expected_min_amount_out(slippage_bps: u32) -> U256 {
7662 expected_min_amount_out_for(&test_pool(), true, slippage_bps)
7663 }
7664
7665 fn expected_min_amount_out_for(pool: &Pool, zero_for_one: bool, slippage_bps: u32) -> U256 {
7666 let profiler = test_profiler(pool, FIXTURE_BLOCK);
7667 let quote = profiler
7668 .swap_exact_in(U256::from(1_000_000_000_000_000u64), zero_for_one, None)
7669 .unwrap();
7670 let quoted = exact_output_amount("e, zero_for_one).unwrap();
7671 derive_min_amount_out(quoted, slippage_bps).unwrap()
7672 }
7673
7674 fn expected_sell_quote_amount() -> U256 {
7675 let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7676 let quote = profiler
7677 .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
7678 .unwrap();
7679 exact_output_amount("e, true).unwrap()
7680 }
7681
7682 fn quote_response(amount: U256) -> String {
7683 let result = (amount, U160::from(1u128 << 96), 0u32, U256::from(50_000u64)).abi_encode();
7684 serde_json::json!({
7685 "jsonrpc": "2.0",
7686 "id": 1,
7687 "result": hex::encode_prefixed(result),
7688 })
7689 .to_string()
7690 }
7691
7692 async fn expected_swap_tx(min_amount_out: U256) -> (B256, String) {
7693 let expected_tx = build_eip1559_transaction(
7696 42161,
7697 7,
7698 78_000,
7699 130_000_000,
7700 10_000_000,
7701 ROUTER_ADDRESS,
7702 U256::ZERO,
7703 Bytes::from(expected_swap_calldata(min_amount_out)),
7704 );
7705 sign_eip1559_transaction(
7706 expected_tx,
7707 &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7708 )
7709 .await
7710 .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7711 .unwrap()
7712 }
7713
7714 fn expected_buy_base_amount() -> U256 {
7715 U256::from(1_000_000_000_000_000u64)
7716 }
7717
7718 fn expected_buy_amount_in() -> U256 {
7719 let profiler = test_profiler(&test_pool(), FIXTURE_BLOCK);
7720 profiler
7721 .swap_exact_out(expected_buy_base_amount(), false, None)
7722 .unwrap()
7723 .get_input_amount()
7724 }
7725
7726 fn expected_buy_min_amount_out(slippage_bps: u32) -> U256 {
7727 derive_min_amount_out(expected_buy_base_amount(), slippage_bps).unwrap()
7728 }
7729
7730 fn expected_buy_swap_calldata(min_amount_out: U256, amount_in: U256) -> Vec<u8> {
7731 UniswapV3SwapRouter::exactInputSingleCall {
7732 params: UniswapV3SwapRouter::ExactInputSingleParams {
7733 tokenIn: USDC_ADDRESS,
7734 tokenOut: WETH_ADDRESS,
7735 fee: U24::try_from(500u32).unwrap(),
7736 recipient: address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266"),
7737 deadline: U256::from(FIXTURE_BLOCK_TIMESTAMP + 300),
7738 amountIn: amount_in,
7739 amountOutMinimum: min_amount_out,
7740 sqrtPriceLimitX96: U160::ZERO,
7741 },
7742 }
7743 .abi_encode()
7744 }
7745
7746 async fn expected_buy_swap_tx(min_amount_out: U256, amount_in: U256) -> (B256, String) {
7747 let expected_tx = build_eip1559_transaction(
7748 42161,
7749 7,
7750 78_000,
7751 130_000_000,
7752 10_000_000,
7753 ROUTER_ADDRESS,
7754 U256::ZERO,
7755 Bytes::from(expected_buy_swap_calldata(min_amount_out, amount_in)),
7756 );
7757 sign_eip1559_transaction(
7758 expected_tx,
7759 &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
7760 )
7761 .await
7762 .map(|(hash, raw)| (hash, nautilus_core::hex::encode_prefixed(&raw)))
7763 .unwrap()
7764 }
7765
7766 fn finalized_buy_swap_receipt(tx_hash: B256, amount_in: U256) -> String {
7767 finalized_buy_swap_receipt_with_base_out(tx_hash, amount_in, expected_buy_base_amount())
7768 }
7769
7770 fn finalized_buy_swap_receipt_with_base_out(
7771 tx_hash: B256,
7772 amount_in: U256,
7773 base_out: U256,
7774 ) -> String {
7775 let data = (
7776 -I256::try_from(u128::try_from(base_out).unwrap()).unwrap(),
7777 I256::try_from(u128::try_from(amount_in).unwrap()).unwrap(),
7778 U160::from(1_u128 << 96),
7779 TEST_LIQUIDITY,
7780 I24::try_from(0).unwrap(),
7781 )
7782 .abi_encode();
7783 serde_json::json!({
7784 "jsonrpc": "2.0",
7785 "id": 1,
7786 "result": {
7787 "transactionHash": tx_hash.to_string(),
7788 "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7789 "blockNumber": "0x1cf0d41",
7790 "transactionIndex": "0x2",
7791 "gasUsed": "0xc3c0",
7792 "effectiveGasPrice": "0x5f5e100",
7793 "status": "0x1",
7794 "logs": [{
7795 "removed": false,
7796 "logIndex": "0x6",
7797 "transactionIndex": "0x2",
7798 "transactionHash": tx_hash.to_string(),
7799 "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7800 "blockNumber": "0x1cf0d41",
7801 "address": test_pool().address.to_string(),
7802 "data": hex::encode_prefixed(data),
7803 "topics": [
7804 "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7805 "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7806 "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7807 ]
7808 }]
7809 }
7810 })
7811 .to_string()
7812 }
7813
7814 fn finalized_buy_swap_block(tx_hash: B256, min_amount_out: U256, amount_in: U256) -> String {
7815 serde_json::json!({
7816 "jsonrpc": "2.0",
7817 "id": 1,
7818 "result": {
7819 "number": "0x1cf0d41",
7820 "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7821 "parentHash": FIXTURE_BLOCK_HASH,
7822 "timestamp": "0x69044a21",
7823 "baseFeePerGas": "0x5f5e100",
7824 "transactions": [{
7825 "hash": tx_hash.to_string(),
7826 "from": WALLET,
7827 "nonce": "0x7",
7828 "chainId": "0xa4b1",
7829 "type": "0x2",
7830 "to": ROUTER,
7831 "input": hex::encode_prefixed(expected_buy_swap_calldata(min_amount_out, amount_in)),
7832 "value": "0x0",
7833 "gas": "0x130b0",
7834 "maxFeePerGas": "0x7bfa480",
7835 "maxPriorityFeePerGas": "0x989680"
7836 }]
7837 }
7838 })
7839 .to_string()
7840 }
7841
7842 fn finalized_buy_swap_rpc_state(
7843 tx_hash: B256,
7844 min_amount_out: U256,
7845 amount_in: U256,
7846 ) -> MockRpcState {
7847 let receipt = finalized_buy_swap_receipt(tx_hash, amount_in);
7848 let block = finalized_buy_swap_block(tx_hash, min_amount_out, amount_in);
7849 with_finalized_identity(
7850 signing_rpc_state()
7851 .with_response("eth_getTransactionReceipt", &receipt)
7852 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7853 .with_send_raw_transaction_echo()
7854 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7855 .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7856 &block,
7857 &receipt,
7858 )
7859 }
7860
7861 fn finalized_swap_receipt(tx_hash: B256) -> String {
7862 let data = (
7863 I256::try_from(1_000_000_000_000_000_i128).unwrap(),
7864 I256::try_from(-1_000_000_i128).unwrap(),
7865 U160::from(1_u128 << 96),
7866 TEST_LIQUIDITY,
7867 I24::try_from(0).unwrap(),
7868 )
7869 .abi_encode();
7870 serde_json::json!({
7871 "jsonrpc": "2.0",
7872 "id": 1,
7873 "result": {
7874 "transactionHash": tx_hash.to_string(),
7875 "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7876 "blockNumber": "0x1cf0d41",
7877 "transactionIndex": "0x2",
7878 "gasUsed": "0xc3c0",
7879 "effectiveGasPrice": "0x5f5e100",
7880 "status": "0x1",
7881 "logs": [{
7882 "removed": false,
7883 "logIndex": "0x6",
7884 "transactionIndex": "0x2",
7885 "transactionHash": tx_hash.to_string(),
7886 "blockHash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7887 "blockNumber": "0x1cf0d41",
7888 "address": test_pool().address.to_string(),
7889 "data": hex::encode_prefixed(data),
7890 "topics": [
7891 "0xc42079f94a6350d7e6235f29174924f928cc2ac818eb64fed8004e115fbcca67",
7892 "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266",
7893 "0x000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266"
7894 ]
7895 }]
7896 }
7897 })
7898 .to_string()
7899 }
7900
7901 fn finalized_swap_receipt_with_unrelated_swap(tx_hash: B256) -> String {
7902 let mut receipt: serde_json::Value =
7903 serde_json::from_str(&finalized_swap_receipt(tx_hash)).unwrap();
7904 let logs = receipt["result"]["logs"].as_array_mut().unwrap();
7905 let mut unrelated = logs[0].clone();
7906 unrelated["address"] = serde_json::json!(ROUTER);
7907 unrelated["logIndex"] = serde_json::json!("0x7");
7908 logs.push(unrelated);
7909 receipt.to_string()
7910 }
7911
7912 fn finalized_swap_block(tx_hash: B256, min_amount_out: U256) -> String {
7913 serde_json::json!({
7914 "jsonrpc": "2.0",
7915 "id": 1,
7916 "result": {
7917 "number": "0x1cf0d41",
7918 "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
7919 "parentHash": FIXTURE_BLOCK_HASH,
7920 "timestamp": "0x69044a21",
7921 "baseFeePerGas": "0x5f5e100",
7922 "transactions": [{
7923 "hash": tx_hash.to_string(),
7924 "from": WALLET,
7925 "nonce": "0x7",
7926 "chainId": "0xa4b1",
7927 "type": "0x2",
7928 "to": ROUTER,
7929 "input": hex::encode_prefixed(expected_swap_calldata(min_amount_out)),
7930 "value": "0x0",
7931 "gas": "0x130b0",
7932 "maxFeePerGas": "0x7bfa480",
7933 "maxPriorityFeePerGas": "0x989680"
7934 }]
7935 }
7936 })
7937 .to_string()
7938 }
7939
7940 fn finalized_swap_rpc_state(tx_hash: B256, min_amount_out: U256) -> MockRpcState {
7941 let receipt = finalized_swap_receipt(tx_hash);
7942 let block = finalized_swap_block(tx_hash, min_amount_out);
7943 with_finalized_identity(
7944 signing_rpc_state()
7945 .with_response("eth_getTransactionReceipt", &receipt)
7946 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
7947 .with_send_raw_transaction_echo()
7948 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
7949 .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE),
7950 &block,
7951 &receipt,
7952 )
7953 }
7954
7955 fn with_finalized_identity(
7956 state: MockRpcState,
7957 block_response: &str,
7958 receipt_response: &str,
7959 ) -> MockRpcState {
7960 let block: serde_json::Value = serde_json::from_str(block_response).unwrap();
7961 let receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
7962 let transaction = block["result"]["transactions"][0].clone();
7963 let transaction_response = serde_json::json!({
7964 "jsonrpc": "2.0",
7965 "id": 1,
7966 "result": transaction,
7967 })
7968 .to_string();
7969 let success = receipt["result"]["status"] == "0x1";
7970 let mut trace = serde_json::json!({
7971 "type": "CALL",
7972 "from": transaction["from"],
7973 "to": transaction["to"],
7974 "value": transaction["value"],
7975 "gas": transaction["gas"],
7976 "gasUsed": receipt["result"]["gasUsed"],
7977 "input": transaction["input"],
7978 "output": "0x",
7979 "calls": [],
7980 });
7981
7982 if !success {
7983 trace["error"] = serde_json::json!("execution reverted");
7984 }
7985 let trace_response = serde_json::json!({
7986 "jsonrpc": "2.0",
7987 "id": 1,
7988 "result": trace,
7989 })
7990 .to_string();
7991 state
7992 .with_response("eth_getTransactionByHash", &transaction_response)
7993 .with_response("debug_traceTransaction", &trace_response)
7994 }
7995
7996 fn receipt_with_transaction_hash(receipt_response: &str, tx_hash: B256) -> String {
7997 let mut receipt: serde_json::Value = serde_json::from_str(receipt_response).unwrap();
7998 receipt["result"]["transactionHash"] = serde_json::json!(tx_hash.to_string());
7999 receipt.to_string()
8000 }
8001
8002 fn replacement_head_block(tx_hash: B256) -> String {
8003 serde_json::json!({
8004 "jsonrpc": "2.0",
8005 "id": 1,
8006 "result": {
8007 "number": "0x1cf0d40",
8008 "hash": "0x1111111111111111111111111111111111111111111111111111111111111111",
8009 "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000001",
8010 "timestamp": "0x69044a20",
8011 "baseFeePerGas": "0x5f5e100",
8012 "transactions": [{
8013 "hash": "0x3333333333333333333333333333333333333333333333333333333333333333",
8014 "from": "0x0000000000000000000000000000000000000001",
8015 "nonce": "0x1",
8016 "type": "0x0",
8017 "to": WETH,
8018 "input": "0x",
8019 "value": "0x0",
8020 "gas": "0x5208"
8021 }, {
8022 "hash": tx_hash.to_string(),
8023 "from": WALLET,
8024 "nonce": "0x7",
8025 "chainId": "0xa4b1",
8026 "type": "0x2",
8027 "to": WETH,
8028 "input": "0xd0e30db0",
8029 "value": "0x38d7ea4c68000",
8030 "gas": "0x130b0",
8031 "maxFeePerGas": "0x7bfa480",
8032 "maxPriorityFeePerGas": "0x989680"
8033 }]
8034 }
8035 })
8036 .to_string()
8037 }
8038
8039 fn finalized_wrap_block(tx_hash: B256) -> String {
8040 serde_json::json!({
8041 "jsonrpc": "2.0",
8042 "id": 1,
8043 "result": {
8044 "number": "0x1cf0d41",
8045 "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8046 "parentHash": FIXTURE_BLOCK_HASH,
8047 "timestamp": "0x69044a21",
8048 "baseFeePerGas": "0x5f5e100",
8049 "transactions": [{
8050 "hash": tx_hash.to_string(),
8051 "from": WALLET,
8052 "nonce": "0x7",
8053 "chainId": "0xa4b1",
8054 "type": "0x2",
8055 "to": WETH,
8056 "input": "0xd0e30db0",
8057 "value": "0x38d7ea4c68000",
8058 "gas": "0x130b0",
8059 "maxFeePerGas": "0x7bfa480",
8060 "maxPriorityFeePerGas": "0x989680"
8061 }]
8062 }
8063 })
8064 .to_string()
8065 }
8066
8067 fn finalized_approve_block(tx_hash: B256, amount: U256) -> String {
8068 let calldata = ERC20::approveCall {
8069 spender: ROUTER_ADDRESS,
8070 amount,
8071 }
8072 .abi_encode();
8073 serde_json::json!({
8074 "jsonrpc": "2.0",
8075 "id": 1,
8076 "result": {
8077 "number": "0x1cf0d41",
8078 "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
8079 "parentHash": FIXTURE_BLOCK_HASH,
8080 "timestamp": "0x69044a21",
8081 "baseFeePerGas": "0x5f5e100",
8082 "transactions": [{
8083 "hash": tx_hash.to_string(),
8084 "from": WALLET,
8085 "nonce": "0x7",
8086 "chainId": "0xa4b1",
8087 "type": "0x2",
8088 "to": WETH,
8089 "input": hex::encode_prefixed(calldata),
8090 "value": "0x0",
8091 "gas": "0x130b0",
8092 "maxFeePerGas": "0x7bfa480",
8093 "maxPriorityFeePerGas": "0x989680"
8094 }]
8095 }
8096 })
8097 .to_string()
8098 }
8099
8100 fn fixture_sell_plan() -> SwapPlan {
8101 let pool = test_pool();
8102 let order = test_market_sell_order(pool.instrument_id);
8103 let quote_token = pool.get_quote_token();
8104 SwapPlan {
8105 order,
8106 quote_currency: Currency::new_checked(
8107 "e_token.symbol,
8108 quote_token.decimals,
8109 0,
8110 "e_token.name,
8111 CurrencyType::Crypto,
8112 )
8113 .unwrap(),
8114 instrument_id: pool.instrument_id,
8115 pool_address: pool.address,
8116 router: ROUTER_ADDRESS,
8117 factory: UNISWAP_V3.dex.factory,
8118 weth: WETH_ADDRESS,
8119 token_in: WETH_ADDRESS,
8120 token_out: USDC_ADDRESS,
8121 fee: U24::try_from(500u32).unwrap(),
8122 amount_in: U256::from(1_000_000_000_000_000u64),
8123 min_amount_out: expected_min_amount_out(50),
8124 slippage_bps: 50,
8125 quote_spend_ceiling: None,
8126 profiler_position: Some(profiler_event_position()),
8127 pool,
8128 }
8129 }
8130
8131 fn fixture_block_response(number: u64, hash: B256) -> String {
8132 let parent_hash = if number == FIXTURE_BLOCK {
8133 B256::from_str("0x0000000000000000000000000000000000000000000000000000000000000001")
8134 .unwrap()
8135 } else {
8136 B256::from_str(FIXTURE_BLOCK_HASH).unwrap()
8137 };
8138 serde_json::json!({
8139 "jsonrpc": "2.0",
8140 "id": 1,
8141 "result": {
8142 "number": format!("0x{number:x}"),
8143 "hash": hash.to_string(),
8144 "parentHash": parent_hash.to_string(),
8145 "timestamp": format!("0x{:x}", FIXTURE_BLOCK_TIMESTAMP + number - FIXTURE_BLOCK),
8146 "baseFeePerGas": "0x5f5e100",
8147 "transactions": []
8148 }
8149 })
8150 .to_string()
8151 }
8152
8153 fn profiler_event_receipt(pool_address: Address) -> String {
8154 let transaction_hash = B256::from([0x33; 32]);
8155 serde_json::json!({
8156 "jsonrpc": "2.0",
8157 "id": 1,
8158 "result": {
8159 "transactionHash": transaction_hash.to_string(),
8160 "blockHash": FIXTURE_BLOCK_HASH,
8161 "blockNumber": "0x1cf0d40",
8162 "transactionIndex": "0x2",
8163 "gasUsed": "0xc3c0",
8164 "effectiveGasPrice": "0x5f5e100",
8165 "status": "0x1",
8166 "logs": [{
8167 "removed": false,
8168 "logIndex": "0x6",
8169 "transactionIndex": "0x2",
8170 "transactionHash": transaction_hash.to_string(),
8171 "blockHash": FIXTURE_BLOCK_HASH,
8172 "blockNumber": "0x1cf0d40",
8173 "address": pool_address.to_string(),
8174 "data": "0x",
8175 "topics": [test_pool().dex.swap_created_event.as_ref()]
8176 }]
8177 }
8178 })
8179 .to_string()
8180 }
8181
8182 fn profiler_event_position() -> BlockPosition {
8183 BlockPosition::new(FIXTURE_BLOCK, B256::from([0x33; 32]).to_string(), 2, 6)
8184 .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()))
8185 }
8186
8187 #[rstest]
8188 fn quantity_to_raw_amount_scales_by_token_decimals() {
8189 assert_eq!(
8190 quantity_to_raw_amount(Quantity::from("0.001"), 18).unwrap(),
8191 U256::from(1_000_000_000_000_000u64)
8192 );
8193 assert_eq!(
8194 quantity_to_raw_amount(Quantity::from("1.5"), 18).unwrap(),
8195 U256::from(1_500_000_000_000_000_000u128)
8196 );
8197 assert_eq!(
8198 quantity_to_raw_amount(Quantity::from("12.5"), 6).unwrap(),
8199 U256::from(12_500_000u64)
8200 );
8201 }
8202
8203 #[rstest]
8204 fn quantity_to_raw_amount_uses_defi_quantity_precision() {
8205 let amount = U256::from(10_000_000_000_000_000u64);
8206 let quantity = Quantity::from_u256(amount, 18).unwrap();
8207
8208 assert_eq!(quantity_to_raw_amount(quantity, 18).unwrap(), amount);
8209 }
8210
8211 #[rstest]
8212 fn quantity_to_raw_amount_rejects_zero() {
8213 let error = quantity_to_raw_amount(Quantity::from("0.0"), 18).unwrap_err();
8214
8215 assert_eq!(error.to_string(), "Order quantity must be positive");
8216 }
8217
8218 #[rstest]
8219 fn quantity_to_raw_amount_rejects_inexact_token_units() {
8220 let error = quantity_to_raw_amount(Quantity::from("0.0000001"), 6).unwrap_err();
8221
8222 assert!(
8223 error
8224 .to_string()
8225 .contains("is not exactly representable in 6 base token decimals"),
8226 "was: {error}"
8227 );
8228 }
8229
8230 #[rstest]
8231 fn raw_amount_to_quantity_inverts_base_quantity() {
8232 let quantity = Quantity::from("0.001");
8233 let amount = quantity_to_raw_amount(quantity, 18).unwrap();
8234
8235 assert_eq!(raw_amount_to_quantity(amount, 18).unwrap(), quantity);
8236 }
8237
8238 #[rstest]
8239 fn raw_amount_to_quantity_rejects_positive_amount_truncated_to_zero() {
8240 let error = raw_amount_to_quantity(U256::from(99u64), 18).unwrap_err();
8241
8242 assert!(
8243 error
8244 .to_string()
8245 .contains("is below representable quantity precision"),
8246 "was: {error}"
8247 );
8248 }
8249
8250 #[rstest]
8251 fn fill_price_from_quote_recovers_spent_quote_after_quantity_truncation() {
8252 let last_qty = raw_amount_to_quantity(U256::from(1_000_000_403_079_044u64), 18).unwrap();
8253 let quote_amount = U256::from(1_891_348u64);
8254 let quote = Currency::new_checked("USDC", 6, 0, "USD Coin", CurrencyType::Crypto).unwrap();
8255 let last_px = fill_price_from_quote(last_qty, quote_amount, quote).unwrap();
8256
8257 assert_eq!(
8258 Money::from_decimal(last_qty.as_decimal() * last_px.as_decimal(), quote).unwrap(),
8259 Money::from_u256(quote_amount, quote).unwrap()
8260 );
8261 }
8262
8263 #[rstest]
8264 fn swap_token_pair_is_directional() {
8265 assert_eq!(
8266 swap_token_pair(OrderSide::Sell, WETH_ADDRESS, USDC_ADDRESS).unwrap(),
8267 (WETH_ADDRESS, USDC_ADDRESS)
8268 );
8269 assert_eq!(
8270 swap_token_pair(OrderSide::Buy, WETH_ADDRESS, USDC_ADDRESS).unwrap(),
8271 (USDC_ADDRESS, WETH_ADDRESS)
8272 );
8273 }
8274
8275 #[rstest]
8276 fn restore_swap_plan_buy_uses_quote_input() {
8277 let (client, cache) =
8278 swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8279 let order = test_market_buy_order(test_pool().instrument_id);
8280 cache
8281 .borrow_mut()
8282 .add_order(order.clone(), None, None, true)
8283 .unwrap();
8284 let amount_in = U256::from(2_345_678u64);
8285 let intent = ExecutionIntentRow {
8286 id: 7,
8287 schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
8288 chain_id: 42161,
8289 wallet_address: WALLET.to_string(),
8290 nonce: Some(7),
8291 purpose: "swap".to_string(),
8292 status: "finalized".to_string(),
8293 client_order_id: Some(order.client_order_id().to_string()),
8294 trader_id: Some(order.trader_id().to_string()),
8295 strategy_id: Some(order.strategy_id().to_string()),
8296 account_id: Some("BLOCKCHAIN-001".to_string()),
8297 instrument_id: Some(order.instrument_id().to_string()),
8298 pool_address: Some(test_pool().address.to_string()),
8299 transaction_to: ROUTER.to_string(),
8300 transaction_input: "0x".to_string(),
8301 transaction_value: "0".to_string(),
8302 amount_in: Some(amount_in.to_string()),
8303 created_block: FIXTURE_BLOCK,
8304 acknowledgement_emitted: true,
8305 fill_emitted: false,
8306 terminal_emitted: false,
8307 active: true,
8308 };
8309
8310 let plan = client.restore_swap_plan(&intent).unwrap();
8311
8312 assert_eq!(plan.token_in, USDC_ADDRESS);
8313 assert_eq!(plan.token_out, WETH_ADDRESS);
8314 assert_eq!(plan.amount_in, amount_in);
8315 }
8316
8317 #[rstest]
8318 #[case(1_000_000, 50, 995_000)]
8319 #[case(1_000_000, 0, 1_000_000)]
8320 #[case(1_000_000, 200, 980_000)]
8321 #[case(10_000, 9_999, 1)]
8322 fn derive_min_amount_out_applies_slippage(
8323 #[case] quoted: u64,
8324 #[case] slippage_bps: u32,
8325 #[case] expected: u64,
8326 ) {
8327 assert_eq!(
8328 derive_min_amount_out(U256::from(quoted), slippage_bps).unwrap(),
8329 U256::from(expected)
8330 );
8331 }
8332
8333 #[rstest]
8334 fn derive_min_amount_out_rejects_zero_result() {
8335 let error = derive_min_amount_out(U256::from(9_999u64), 9_999).unwrap_err();
8336
8337 assert!(
8338 error.to_string().contains("Derived minimum output is zero"),
8339 "was: {error}"
8340 );
8341 }
8342
8343 #[rstest]
8344 fn derive_min_amount_out_rejects_full_slippage() {
8345 let error = derive_min_amount_out(U256::from(1_000_000u64), 10_000).unwrap_err();
8346
8347 assert!(
8348 error.to_string().contains("must be below 10000"),
8349 "was: {error}"
8350 );
8351 }
8352
8353 #[rstest]
8354 fn replacement_scan_range_is_bounded_and_checked() {
8355 assert_eq!(
8356 replacement_scan_range(10, 10).unwrap(),
8357 RangeInclusive::new(10, 10)
8358 );
8359 assert_eq!(
8360 replacement_scan_range(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS).unwrap(),
8361 RangeInclusive::new(10, 10 + MAX_REPLACEMENT_SCAN_BLOCKS - 1)
8362 );
8363 assert!(
8364 replacement_scan_range(11, 10)
8365 .unwrap_err()
8366 .to_string()
8367 .contains("is behind execution creation block")
8368 );
8369 assert_eq!(
8370 replacement_scan_range(u64::MAX - 1, u64::MAX).unwrap(),
8371 RangeInclusive::new(u64::MAX - 1, u64::MAX)
8372 );
8373 }
8374
8375 #[rstest]
8376 fn terminal_execution_event_ids_are_stable_and_kind_specific() {
8377 let transaction_hash = B256::from([0x42; 32]);
8378
8379 let fill = execution_event_id(transaction_hash, b"fill");
8380 let fill_retry = execution_event_id(transaction_hash, b"fill");
8381 let reverted = execution_event_id(transaction_hash, b"reverted");
8382
8383 assert_eq!(fill, fill_retry);
8384 assert_ne!(fill, reverted);
8385 }
8386
8387 #[rstest]
8388 fn prepared_transaction_debug_redacts_raw_transaction() {
8389 let prepared = PreparedTransaction {
8390 intent_id: 1,
8391 created_block: 2,
8392 nonce: 3,
8393 tx_hash: B256::ZERO,
8394 raw_tx: vec![0xde, 0xad, 0xbe, 0xef],
8395 payload_lease: None,
8396 };
8397
8398 let debug = format!("{prepared:?}");
8399
8400 assert!(debug.contains("raw_tx: \"<redacted>\""));
8401 assert!(!debug.contains("[222, 173, 190, 239]"));
8402 }
8403
8404 #[rstest]
8405 fn call_trace_rejects_unreviewed_internal_edge() {
8406 let signed = crate::execution::transaction::DecodedSignedTransaction {
8407 hash: B256::from([1; 32]),
8408 signer: Address::from_str(WALLET).unwrap(),
8409 chain_id: 42_161,
8410 nonce: 7,
8411 to: ROUTER_ADDRESS,
8412 value: U256::ZERO,
8413 input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8414 gas_limit: 78_000,
8415 max_fee_per_gas: 130_000_000,
8416 max_priority_fee_per_gas: 10_000_000,
8417 };
8418 let input_digest = keccak256(&signed.input);
8419 let trace = VerifiedCallTrace {
8420 call_type: RpcCallType::Call,
8421 from: signed.signer,
8422 to: Some(signed.to),
8423 value: signed.value,
8424 input_selector: signed
8425 .input
8426 .get(..4)
8427 .map(|selector| selector.try_into().unwrap()),
8428 input_digest,
8429 success: true,
8430 calls: vec![VerifiedCallTrace {
8431 call_type: RpcCallType::Call,
8432 from: ROUTER_ADDRESS,
8433 to: Some(WETH_ADDRESS),
8434 value: U256::ZERO,
8435 input_selector: None,
8436 input_digest: B256::ZERO,
8437 success: true,
8438 calls: Vec::new(),
8439 }],
8440 };
8441 let manifest = &test_config("http://127.0.0.1:1".to_string())
8442 .verification
8443 .unwrap()
8444 .deployment_manifest;
8445
8446 let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8447
8448 assert!(
8449 error.to_string().contains("unreviewed call edge"),
8450 "was: {error}"
8451 );
8452 }
8453
8454 #[rstest]
8455 fn call_trace_rejects_unlisted_precompile_target() {
8456 let manifest = test_config("http://127.0.0.1:1".to_string())
8457 .verification
8458 .unwrap()
8459 .deployment_manifest;
8460 let calls = [VerifiedCallTrace {
8461 call_type: RpcCallType::Call,
8462 from: ROUTER_ADDRESS,
8463 to: Some(address!("0000000000000000000000000000000000000007")),
8464 value: U256::ZERO,
8465 input_selector: None,
8466 input_digest: B256::ZERO,
8467 success: true,
8468 calls: Vec::new(),
8469 }];
8470
8471 let error =
8472 validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8473
8474 assert_eq!(
8475 error.to_string(),
8476 format!(
8477 "Verified call trace contains an unreviewed call edge {ROUTER_ADDRESS} -> {} for swap_sell",
8478 address!("0000000000000000000000000000000000000007")
8479 )
8480 );
8481 }
8482
8483 #[rstest]
8484 fn call_trace_requires_exact_call_type() {
8485 let manifest = test_config("http://127.0.0.1:1".to_string())
8486 .verification
8487 .unwrap()
8488 .deployment_manifest;
8489 let calls = [VerifiedCallTrace {
8490 call_type: RpcCallType::Staticcall,
8491 from: ROUTER_ADDRESS,
8492 to: Some(test_pool().address),
8493 value: U256::ZERO,
8494 input_selector: None,
8495 input_digest: B256::ZERO,
8496 success: true,
8497 calls: Vec::new(),
8498 }];
8499
8500 let error =
8501 validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8502
8503 assert!(
8504 error.to_string().contains("unreviewed staticcall edge"),
8505 "was: {error}"
8506 );
8507 }
8508
8509 #[rstest]
8510 fn call_trace_requires_exact_caller() {
8511 let manifest = test_config("http://127.0.0.1:1".to_string())
8512 .verification
8513 .unwrap()
8514 .deployment_manifest;
8515 let calls = [VerifiedCallTrace {
8516 call_type: RpcCallType::Call,
8517 from: WETH_ADDRESS,
8518 to: Some(test_pool().address),
8519 value: U256::ZERO,
8520 input_selector: None,
8521 input_digest: B256::ZERO,
8522 success: true,
8523 calls: Vec::new(),
8524 }];
8525
8526 let error =
8527 validate_internal_calls(&calls, ROUTER_ADDRESS, "swap_sell", &manifest).unwrap_err();
8528
8529 assert_eq!(
8530 error.to_string(),
8531 "Verified call trace child has an invalid caller context"
8532 );
8533 }
8534
8535 #[rstest]
8536 fn call_trace_rejects_contract_creation() {
8537 let signed = crate::execution::transaction::DecodedSignedTransaction {
8538 hash: B256::from([1; 32]),
8539 signer: Address::from_str(WALLET).unwrap(),
8540 chain_id: 42_161,
8541 nonce: 7,
8542 to: ROUTER_ADDRESS,
8543 value: U256::ZERO,
8544 input: Bytes::from(expected_swap_calldata(expected_min_amount_out(50))),
8545 gas_limit: 78_000,
8546 max_fee_per_gas: 130_000_000,
8547 max_priority_fee_per_gas: 10_000_000,
8548 };
8549 let trace = VerifiedCallTrace {
8550 call_type: RpcCallType::Call,
8551 from: signed.signer,
8552 to: Some(signed.to),
8553 value: signed.value,
8554 input_selector: signed
8555 .input
8556 .get(..4)
8557 .map(|selector| selector.try_into().unwrap()),
8558 input_digest: keccak256(&signed.input),
8559 success: true,
8560 calls: vec![VerifiedCallTrace {
8561 call_type: RpcCallType::Create,
8562 from: ROUTER_ADDRESS,
8563 to: Some(address!("0000000000000000000000000000000000000007")),
8564 value: U256::ZERO,
8565 input_selector: None,
8566 input_digest: B256::ZERO,
8567 success: true,
8568 calls: Vec::new(),
8569 }],
8570 };
8571 let manifest = &test_config("http://127.0.0.1:1".to_string())
8572 .verification
8573 .unwrap()
8574 .deployment_manifest;
8575
8576 let error = validate_call_trace(&trace, &signed, true, "swap_sell", manifest).unwrap_err();
8577
8578 assert!(
8579 error
8580 .to_string()
8581 .contains("forbidden state-changing operation"),
8582 "was: {error}"
8583 );
8584 }
8585
8586 #[tokio::test]
8587 async fn swap_quote_rejects_missing_ingestion_block_hash() {
8588 let (client, state) = client_with_mock_rpc(execution_rpc_state()).await;
8589 let plan = fixture_sell_plan();
8590 let position = BlockPosition::new(
8591 FIXTURE_BLOCK,
8592 FIXTURE_BLOCK_HASH.to_string(),
8593 BLOCK_SCOPED_SNAPSHOT_INDEX,
8594 BLOCK_SCOPED_SNAPSHOT_INDEX,
8595 );
8596
8597 let error = validate_swap_quote(
8598 &position,
8599 &plan,
8600 100,
8601 &client.verification,
8602 &client
8603 .config
8604 .verification
8605 .as_ref()
8606 .unwrap()
8607 .deployment_manifest,
8608 )
8609 .await
8610 .unwrap_err();
8611
8612 assert!(
8613 error.to_string().contains("no ingestion-time block hash"),
8614 "was: {error}"
8615 );
8616 assert!(state.recorded_requests().is_empty());
8617 }
8618
8619 #[tokio::test]
8620 async fn swap_quote_rejects_replaced_ingestion_block() {
8621 let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
8622 let state = execution_rpc_state().with_parameter_response(
8623 "eth_getBlockByNumber",
8624 "0x1cf0d40",
8625 &changed,
8626 );
8627 let (client, _) = client_with_mock_rpc(state).await;
8628 let plan = fixture_sell_plan();
8629 let position = BlockPosition::new(
8630 FIXTURE_BLOCK,
8631 FIXTURE_BLOCK_HASH.to_string(),
8632 BLOCK_SCOPED_SNAPSHOT_INDEX,
8633 BLOCK_SCOPED_SNAPSHOT_INDEX,
8634 )
8635 .with_block_hash(Some(FIXTURE_BLOCK_HASH.to_string()));
8636
8637 let error = validate_swap_quote(
8638 &position,
8639 &plan,
8640 100,
8641 &client.verification,
8642 &client
8643 .config
8644 .verification
8645 .as_ref()
8646 .unwrap()
8647 .deployment_manifest,
8648 )
8649 .await
8650 .unwrap_err();
8651
8652 assert!(error.to_string().contains("changed from"), "was: {error}");
8653 }
8654
8655 #[tokio::test]
8656 async fn swap_quote_accepts_exact_canonical_event_watermark() {
8657 let pool = test_pool();
8658 let receipt = profiler_event_receipt(pool.address);
8659 let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8660 let (client, _) = client_with_mock_rpc(state).await;
8661 let plan = fixture_sell_plan();
8662
8663 let anchors = validate_swap_quote(
8664 &profiler_event_position(),
8665 &plan,
8666 100,
8667 &client.verification,
8668 &client
8669 .config
8670 .verification
8671 .as_ref()
8672 .unwrap()
8673 .deployment_manifest,
8674 )
8675 .await
8676 .unwrap();
8677
8678 assert_eq!(anchors.watermark.number, profiler_event_position().number);
8679 assert_eq!(anchors.state.number, FIXTURE_BLOCK);
8680 assert_eq!(anchors.state.hash, B256::from([0x11; 32]));
8681 assert_eq!(anchors.state.timestamp, FIXTURE_BLOCK_TIMESTAMP);
8682 }
8683
8684 #[tokio::test]
8685 async fn swap_quote_rejects_mismatched_receipt_position() {
8686 let pool = test_pool();
8687 let mut receipt: serde_json::Value =
8688 serde_json::from_str(&profiler_event_receipt(pool.address)).unwrap();
8689 receipt["result"]["transactionIndex"] = serde_json::json!("0x3");
8690 let state =
8691 execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt.to_string());
8692 let (client, _) = client_with_mock_rpc(state).await;
8693 let plan = fixture_sell_plan();
8694
8695 let error = validate_swap_quote(
8696 &profiler_event_position(),
8697 &plan,
8698 100,
8699 &client.verification,
8700 &client
8701 .config
8702 .verification
8703 .as_ref()
8704 .unwrap()
8705 .deployment_manifest,
8706 )
8707 .await
8708 .unwrap_err();
8709
8710 assert_eq!(
8711 error.to_string(),
8712 "Profiler receipt position does not match its ingestion watermark"
8713 );
8714 }
8715
8716 #[tokio::test]
8717 async fn swap_quote_rejects_watermark_from_different_pool() {
8718 let receipt = profiler_event_receipt(ROUTER_ADDRESS);
8719 let state = execution_rpc_state().with_response("eth_getTransactionReceipt", &receipt);
8720 let (client, _) = client_with_mock_rpc(state).await;
8721 let plan = fixture_sell_plan();
8722
8723 let error = validate_swap_quote(
8724 &profiler_event_position(),
8725 &plan,
8726 100,
8727 &client.verification,
8728 &client
8729 .config
8730 .verification
8731 .as_ref()
8732 .unwrap()
8733 .deployment_manifest,
8734 )
8735 .await
8736 .unwrap_err();
8737
8738 assert!(
8739 error
8740 .to_string()
8741 .contains("did not come from expected pool"),
8742 "was: {error}"
8743 );
8744 }
8745
8746 #[rstest]
8747 fn verified_sell_quote_sets_signed_amounts() {
8748 let plan = fixture_sell_plan();
8749 let quote = UniswapV3Quote {
8750 amount: expected_sell_quote_amount(),
8751 sqrt_price_x96_after: U160::from(1u128 << 96),
8752 initialized_ticks_crossed: 0,
8753 gas_estimate: U256::from(50_000u64),
8754 };
8755
8756 let (amount_in, min_amount_out) = verified_swap_amounts(&plan, quote).unwrap();
8757
8758 assert_eq!(amount_in, U256::from(1_000_000_000_000_000u64));
8759 assert_eq!(min_amount_out, expected_min_amount_out(50));
8760 }
8761
8762 #[rstest]
8763 fn exact_output_amount_extracts_negative_leg() {
8764 let pool = test_pool();
8765 let profiler = test_profiler(&pool, FIXTURE_BLOCK);
8766 let quote = profiler
8767 .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
8768 .unwrap();
8769
8770 let amount = exact_output_amount("e, true).unwrap();
8771
8772 assert!(amount < U256::from(1_000_000_000_000_000u64));
8773 assert!(amount > U256::from(990_000_000_000_000u64));
8774
8775 let error = exact_output_amount("e, false).unwrap_err();
8776 assert!(
8777 error.to_string().contains("is not a positive output"),
8778 "was: {error}"
8779 );
8780 }
8781
8782 #[rstest]
8783 #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV3", true)]
8784 #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3", false)]
8785 #[case("0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4", false)]
8786 #[case("ETHUSDT-PERP.BINANCE", false)]
8787 fn handles_order_venue_matches_chain_and_dex(#[case] instrument: &str, #[case] expected: bool) {
8788 let client = test_client("http://127.0.0.1:1".to_string());
8789 let instrument_id: InstrumentId = instrument.parse().unwrap();
8790
8791 assert_eq!(client.handles_order_venue(instrument_id.venue), expected);
8792 }
8793
8794 #[tokio::test]
8795 async fn submit_order_denies_buy_without_allowlisted_pair() {
8796 let (mut client, cache) =
8797 swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
8798 let pool = test_pool();
8799 let order = test_market_buy_order(pool.instrument_id);
8800 cache
8801 .borrow_mut()
8802 .add_order(order.clone(), None, None, true)
8803 .unwrap();
8804 let mut receiver = start_with_events(&mut client);
8805
8806 client.submit_order(submit_order_cmd(&order)).unwrap();
8807
8808 let events = collect_order_events(&mut receiver);
8809 assert_eq!(events.len(), 1);
8810 let OrderEventAny::Denied(denied) = &events[0] else {
8811 panic!("expected OrderDenied, was {:?}", events[0]);
8812 };
8813 assert!(
8814 denied
8815 .reason
8816 .contains("not in the `allowed_token_pairs` allowlist"),
8817 "was: {}",
8818 denied.reason
8819 );
8820 assert!(
8821 denied.reason.contains(&USDC_ADDRESS.to_string()),
8822 "was: {}",
8823 denied.reason
8824 );
8825 }
8826
8827 #[tokio::test]
8828 async fn submit_order_denies_buy_quote_denominated_quantity() {
8829 let (mut client, cache) =
8830 swap_client_with_cache(buy_test_config("http://127.0.0.1:1".to_string()));
8831 let pool = test_pool();
8832 let order = OrderTestBuilder::new(OrderType::Market)
8833 .trader_id(TraderId::from("TRADER-001"))
8834 .strategy_id(StrategyId::from("S-001"))
8835 .instrument_id(pool.instrument_id)
8836 .client_order_id(ClientOrderId::from("O-SWAP-BUY-001"))
8837 .side(OrderSide::Buy)
8838 .quantity(Quantity::from("0.001"))
8839 .quote_quantity(true)
8840 .build();
8841 cache
8842 .borrow_mut()
8843 .add_order(order.clone(), None, None, true)
8844 .unwrap();
8845 let mut receiver = start_with_events(&mut client);
8846
8847 client.submit_order(submit_order_cmd(&order)).unwrap();
8848
8849 let events = collect_order_events(&mut receiver);
8850 assert_eq!(events.len(), 1);
8851 let OrderEventAny::Denied(denied) = &events[0] else {
8852 panic!("expected OrderDenied, was {:?}", events[0]);
8853 };
8854 assert!(
8855 denied.reason.contains("Quote-denominated"),
8856 "was: {}",
8857 denied.reason
8858 );
8859 }
8860
8861 #[tokio::test]
8862 async fn submit_order_denies_buy_amount_above_max_order_amount() {
8863 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8864 config.max_order_amount = Some(999_999_999_999_999);
8865 let (mut client, cache) = swap_client_with_cache(config);
8866 let order = test_market_buy_order(test_pool().instrument_id);
8867 cache
8868 .borrow_mut()
8869 .add_order(order.clone(), None, None, true)
8870 .unwrap();
8871 let mut receiver = start_with_events(&mut client);
8872
8873 client.submit_order(submit_order_cmd(&order)).unwrap();
8874
8875 let events = collect_order_events(&mut receiver);
8876 assert_eq!(events.len(), 1);
8877 let OrderEventAny::Denied(denied) = &events[0] else {
8878 panic!("expected OrderDenied, was {:?}", events[0]);
8879 };
8880 assert!(
8881 denied
8882 .reason
8883 .contains("exceeds the configured `max_order_amount`"),
8884 "was: {}",
8885 denied.reason
8886 );
8887 }
8888
8889 #[tokio::test]
8890 async fn submit_order_denies_buy_without_quote_spend_limit() {
8891 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8892 config.quote_spend_limits = None;
8893 let (mut client, cache) = swap_client_with_cache(config);
8894 let order = test_market_buy_order(test_pool().instrument_id);
8895 cache
8896 .borrow_mut()
8897 .add_order(order.clone(), None, None, true)
8898 .unwrap();
8899 let mut receiver = start_with_events(&mut client);
8900
8901 client.submit_order(submit_order_cmd(&order)).unwrap();
8902
8903 let events = collect_order_events(&mut receiver);
8904 assert_eq!(events.len(), 1);
8905 let OrderEventAny::Denied(denied) = &events[0] else {
8906 panic!("expected OrderDenied, was {:?}", events[0]);
8907 };
8908 assert!(
8909 denied
8910 .reason
8911 .contains("No `quote_spend_limits` entry for BUY token pair"),
8912 "was: {}",
8913 denied.reason
8914 );
8915 }
8916
8917 #[tokio::test]
8918 async fn submit_order_uses_pair_specific_quote_spend_limit() {
8919 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8920 config.quote_spend_limits = Some(vec![quote_spend_limit(
8921 WETH,
8922 USDC,
8923 18,
8924 "1000000000000000000",
8925 )]);
8926 let (mut client, cache) = swap_client_with_cache(config);
8927 let order = test_market_buy_order(test_pool().instrument_id);
8928 cache
8929 .borrow_mut()
8930 .add_order(order.clone(), None, None, true)
8931 .unwrap();
8932 let mut receiver = start_with_events(&mut client);
8933
8934 client.submit_order(submit_order_cmd(&order)).unwrap();
8935
8936 let events = collect_order_events(&mut receiver);
8937 assert_eq!(events.len(), 1);
8938 let OrderEventAny::Denied(denied) = &events[0] else {
8939 panic!("expected OrderDenied, was {:?}", events[0]);
8940 };
8941 assert!(
8942 denied
8943 .reason
8944 .contains("No `quote_spend_limits` entry for BUY token pair"),
8945 "was: {}",
8946 denied.reason
8947 );
8948 }
8949
8950 #[tokio::test]
8951 async fn submit_order_denies_buy_with_quote_spend_precision_mismatch() {
8952 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8953 config.quote_spend_limits.as_mut().unwrap()[0].spend_token_decimals = 18;
8954 let error = test_client_result(config, test_pool()).unwrap_err();
8955 assert!(
8956 error
8957 .to_string()
8958 .contains("Quote spend limit decimals do not match the deployment manifest"),
8959 "was: {error}"
8960 );
8961 }
8962
8963 #[tokio::test]
8964 async fn submit_order_denies_buy_with_zero_quote_spend_limit() {
8965 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8966 config.quote_spend_limits.as_mut().unwrap()[0].max_amount = "0".to_string();
8967 let (mut client, cache) = swap_client_with_cache(config);
8968 let order = test_market_buy_order(test_pool().instrument_id);
8969 cache
8970 .borrow_mut()
8971 .add_order(order.clone(), None, None, true)
8972 .unwrap();
8973 let mut receiver = start_with_events(&mut client);
8974
8975 client.submit_order(submit_order_cmd(&order)).unwrap();
8976
8977 let events = collect_order_events(&mut receiver);
8978 assert_eq!(events.len(), 1);
8979 let OrderEventAny::Denied(denied) = &events[0] else {
8980 panic!("expected OrderDenied, was {:?}", events[0]);
8981 };
8982 assert!(
8983 denied
8984 .reason
8985 .contains("exceeds the configured `quote_spend_limits` maximum 0"),
8986 "was: {}",
8987 denied.reason
8988 );
8989 }
8990
8991 #[tokio::test]
8992 async fn submit_order_denies_buy_one_raw_unit_above_quote_spend_limit_before_readiness() {
8993 let amount_in = expected_buy_amount_in();
8994 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
8995 config.quote_spend_limits.as_mut().unwrap()[0].max_amount =
8996 (amount_in - U256::from(1u8)).to_string();
8997 let (mut client, cache) = swap_client_with_cache(config);
8998 let order = test_market_buy_order(test_pool().instrument_id);
8999 cache
9000 .borrow_mut()
9001 .add_order(order.clone(), None, None, true)
9002 .unwrap();
9003 let mut receiver = start_with_events(&mut client);
9004
9005 client.submit_order(submit_order_cmd(&order)).unwrap();
9006
9007 let events = collect_order_events(&mut receiver);
9008 assert_eq!(events.len(), 1);
9009 let OrderEventAny::Denied(denied) = &events[0] else {
9010 panic!("expected OrderDenied, was {:?}", events[0]);
9011 };
9012 assert!(
9013 denied.reason.contains(&format!(
9014 "BUY quote amount {amount_in} exceeds the configured `quote_spend_limits`"
9015 )),
9016 "was: {}",
9017 denied.reason
9018 );
9019 assert!(!client.core.is_connected());
9020 assert!(!client.cache.has_database());
9021 assert!(client.signer.is_none());
9022 assert!(client.pending_tasks.is_empty());
9023 }
9024
9025 #[tokio::test]
9026 async fn submit_order_denies_non_market_order_type() {
9027 let (mut client, cache) =
9028 swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9029 let pool = test_pool();
9030 let order = OrderTestBuilder::new(OrderType::Limit)
9031 .trader_id(TraderId::from("TRADER-001"))
9032 .strategy_id(StrategyId::from("S-001"))
9033 .instrument_id(pool.instrument_id)
9034 .client_order_id(ClientOrderId::from("O-SWAP-001"))
9035 .side(OrderSide::Sell)
9036 .quantity(Quantity::from("0.001"))
9037 .price(Price::from("2000"))
9038 .build();
9039 cache
9040 .borrow_mut()
9041 .add_order(order.clone(), None, None, true)
9042 .unwrap();
9043 let mut receiver = start_with_events(&mut client);
9044
9045 client.submit_order(submit_order_cmd(&order)).unwrap();
9046
9047 let events = collect_order_events(&mut receiver);
9048 assert_eq!(events.len(), 1);
9049 let OrderEventAny::Denied(denied) = &events[0] else {
9050 panic!("expected OrderDenied, was {:?}", events[0]);
9051 };
9052 assert!(
9053 denied.reason.contains("only Market is supported"),
9054 "was: {}",
9055 denied.reason
9056 );
9057 }
9058
9059 #[tokio::test]
9060 async fn submit_order_denies_quote_denominated_quantity() {
9061 let (mut client, cache) =
9062 swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9063 let pool = test_pool();
9064 let order = OrderTestBuilder::new(OrderType::Market)
9065 .trader_id(TraderId::from("TRADER-001"))
9066 .strategy_id(StrategyId::from("S-001"))
9067 .instrument_id(pool.instrument_id)
9068 .client_order_id(ClientOrderId::from("O-SWAP-001"))
9069 .side(OrderSide::Sell)
9070 .quantity(Quantity::from("0.001"))
9071 .quote_quantity(true)
9072 .build();
9073 cache
9074 .borrow_mut()
9075 .add_order(order.clone(), None, None, true)
9076 .unwrap();
9077 let mut receiver = start_with_events(&mut client);
9078
9079 client.submit_order(submit_order_cmd(&order)).unwrap();
9080
9081 let events = collect_order_events(&mut receiver);
9082 assert_eq!(events.len(), 1);
9083 let OrderEventAny::Denied(denied) = &events[0] else {
9084 panic!("expected OrderDenied, was {:?}", events[0]);
9085 };
9086 assert!(
9087 denied.reason.contains("Quote-denominated"),
9088 "was: {}",
9089 denied.reason
9090 );
9091 }
9092
9093 #[tokio::test]
9094 async fn submit_order_denies_unknown_pool() {
9095 let (mut client, cache) =
9096 swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9097 let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
9098 .parse()
9099 .unwrap();
9100 let order = OrderTestBuilder::new(OrderType::Market)
9101 .trader_id(TraderId::from("TRADER-001"))
9102 .strategy_id(StrategyId::from("S-001"))
9103 .instrument_id(unknown)
9104 .client_order_id(ClientOrderId::from("O-SWAP-001"))
9105 .side(OrderSide::Sell)
9106 .quantity(Quantity::from("0.001"))
9107 .build();
9108 cache
9109 .borrow_mut()
9110 .add_order(order.clone(), None, None, true)
9111 .unwrap();
9112 let mut receiver = start_with_events(&mut client);
9113
9114 client.submit_order(submit_order_cmd(&order)).unwrap();
9115
9116 let events = collect_order_events(&mut receiver);
9117 assert_eq!(events.len(), 1);
9118 let OrderEventAny::Denied(denied) = &events[0] else {
9119 panic!("expected OrderDenied, was {:?}", events[0]);
9120 };
9121 assert!(
9122 denied.reason.contains("Unknown pool"),
9123 "was: {}",
9124 denied.reason
9125 );
9126 }
9127
9128 #[tokio::test]
9129 async fn submit_order_denies_sell_when_only_buy_pair_allowlisted() {
9130 let mut config = test_config("http://127.0.0.1:1".to_string());
9131 config.allowed_token_pairs = Some(vec![(USDC.to_string(), WETH.to_string())]);
9132 let (mut client, _) = swap_client_with_cache(config);
9133 let order = test_market_sell_order(test_pool().instrument_id);
9134 let mut receiver = start_with_events(&mut client);
9135
9136 client.submit_order(submit_order_cmd(&order)).unwrap();
9137
9138 let events = collect_order_events(&mut receiver);
9139 assert_eq!(events.len(), 1);
9140 let OrderEventAny::Denied(denied) = &events[0] else {
9141 panic!("expected OrderDenied, was {:?}", events[0]);
9142 };
9143 assert!(
9144 denied
9145 .reason
9146 .contains("not in the `allowed_token_pairs` allowlist"),
9147 "was: {}",
9148 denied.reason
9149 );
9150 assert!(
9151 denied.reason.contains(&WETH_ADDRESS.to_string()),
9152 "was: {}",
9153 denied.reason
9154 );
9155 }
9156
9157 #[tokio::test]
9158 async fn submit_order_sell_ignores_quote_spend_limits() {
9159 let mut config = test_config("http://127.0.0.1:1".to_string());
9160 config.quote_spend_limits = Some(vec![quote_spend_limit(WETH, USDC, 18, "0")]);
9161 let (mut client, _) = swap_client_with_cache(config);
9162 let order = test_market_sell_order(test_pool().instrument_id);
9163 let mut receiver = start_with_events(&mut client);
9164
9165 client.submit_order(submit_order_cmd(&order)).unwrap();
9166
9167 let events = collect_order_events(&mut receiver);
9168 assert_eq!(events.len(), 1);
9169 let OrderEventAny::Denied(denied) = &events[0] else {
9170 panic!("expected OrderDenied, was {:?}", events[0]);
9171 };
9172 assert!(
9173 denied
9174 .reason
9175 .contains("Blockchain execution client is not connected"),
9176 "was: {}",
9177 denied.reason
9178 );
9179 }
9180
9181 #[tokio::test]
9182 async fn submit_order_denies_token_pair_outside_allowlist() {
9183 let mut config = test_config("http://127.0.0.1:1".to_string());
9184 config.allowed_token_pairs = Some(Vec::new());
9185 let (mut client, _) = swap_client_with_cache(config);
9186 let order = test_market_sell_order(test_pool().instrument_id);
9187 let mut receiver = start_with_events(&mut client);
9188
9189 client.submit_order(submit_order_cmd(&order)).unwrap();
9190
9191 let events = collect_order_events(&mut receiver);
9192 assert_eq!(events.len(), 1);
9193 let OrderEventAny::Denied(denied) = &events[0] else {
9194 panic!("expected OrderDenied, was {:?}", events[0]);
9195 };
9196 assert!(
9197 denied
9198 .reason
9199 .contains("not in the `allowed_token_pairs` allowlist"),
9200 "was: {}",
9201 denied.reason
9202 );
9203 }
9204
9205 #[tokio::test]
9206 async fn submit_order_denies_amount_above_max_order_amount() {
9207 let mut config = test_config("http://127.0.0.1:1".to_string());
9208 config.max_order_amount = Some(999_999_999_999_999); let (mut client, _) = swap_client_with_cache(config);
9210 let order = test_market_sell_order(test_pool().instrument_id);
9211 let mut receiver = start_with_events(&mut client);
9212
9213 client.submit_order(submit_order_cmd(&order)).unwrap();
9214
9215 let events = collect_order_events(&mut receiver);
9216 assert_eq!(events.len(), 1);
9217 let OrderEventAny::Denied(denied) = &events[0] else {
9218 panic!("expected OrderDenied, was {:?}", events[0]);
9219 };
9220 assert!(
9221 denied
9222 .reason
9223 .contains("exceeds the configured `max_order_amount`"),
9224 "was: {}",
9225 denied.reason
9226 );
9227 }
9228
9229 #[tokio::test]
9230 async fn submit_order_denies_slippage_param_above_ceiling() {
9231 let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9232 let order = test_market_sell_order(test_pool().instrument_id);
9233 let mut cmd = submit_order_cmd(&order);
9234 cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 201}"#).unwrap());
9235 let mut receiver = start_with_events(&mut client);
9236
9237 client.submit_order(cmd).unwrap();
9238
9239 let events = collect_order_events(&mut receiver);
9240 assert_eq!(events.len(), 1);
9241 let OrderEventAny::Denied(denied) = &events[0] else {
9242 panic!("expected OrderDenied, was {:?}", events[0]);
9243 };
9244 assert!(
9245 denied
9246 .reason
9247 .contains("exceeds the configured `max_slippage_bps`"),
9248 "was: {}",
9249 denied.reason
9250 );
9251 }
9252
9253 #[tokio::test]
9254 async fn submit_order_denies_pool_without_fee_tier() {
9255 let addr = start_mock_rpc_server(MockRpcState::default()).await;
9256 let mut pool = test_pool();
9257 pool.fee = None;
9258 let cache = Rc::new(RefCell::new(Cache::default()));
9259 cache.borrow_mut().add_pool(pool.clone()).unwrap();
9260 let order = test_market_sell_order(pool.instrument_id);
9261 cache
9262 .borrow_mut()
9263 .add_order(order.clone(), None, None, false)
9264 .unwrap();
9265 let core = ExecutionClientCore::new(
9266 TraderId::from("TRADER-001"),
9267 ClientId::from("BLOCKCHAIN-001"),
9268 *BLOCKCHAIN_VENUE,
9269 OmsType::Netting,
9270 AccountId::from("BLOCKCHAIN-001"),
9271 AccountType::Wallet,
9272 None,
9273 cache,
9274 );
9275 let mut client =
9276 BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9277 let mut receiver = start_with_events(&mut client);
9278
9279 client.submit_order(submit_order_cmd(&order)).unwrap();
9280
9281 let events = collect_order_events(&mut receiver);
9282 assert_eq!(events.len(), 1);
9283 let OrderEventAny::Denied(denied) = &events[0] else {
9284 panic!("expected OrderDenied, was {:?}", events[0]);
9285 };
9286 assert!(
9287 denied.reason.contains("no fee tier"),
9288 "was: {}",
9289 denied.reason
9290 );
9291 }
9292
9293 #[tokio::test]
9294 async fn submit_order_denies_without_live_profiler() {
9295 let addr = start_mock_rpc_server(MockRpcState::default()).await;
9296 let cache = Rc::new(RefCell::new(Cache::default()));
9297 let pool = test_pool();
9298 cache.borrow_mut().add_pool(pool.clone()).unwrap();
9299 let order = test_market_sell_order(pool.instrument_id);
9300 cache
9301 .borrow_mut()
9302 .add_order(order.clone(), None, None, false)
9303 .unwrap();
9304 let core = ExecutionClientCore::new(
9305 TraderId::from("TRADER-001"),
9306 ClientId::from("BLOCKCHAIN-001"),
9307 *BLOCKCHAIN_VENUE,
9308 OmsType::Netting,
9309 AccountId::from("BLOCKCHAIN-001"),
9310 AccountType::Wallet,
9311 None,
9312 cache,
9313 );
9314 let mut client =
9315 BlockchainExecutionClient::new(core, test_config(format!("http://{addr}"))).unwrap();
9316 let mut receiver = start_with_events(&mut client);
9317
9318 client.submit_order(submit_order_cmd(&order)).unwrap();
9319
9320 let events = collect_order_events(&mut receiver);
9321 assert_eq!(events.len(), 1);
9322 let OrderEventAny::Denied(denied) = &events[0] else {
9323 panic!("expected OrderDenied, was {:?}", events[0]);
9324 };
9325 assert!(
9326 denied.reason.contains("No pool profiler"),
9327 "was: {}",
9328 denied.reason
9329 );
9330 }
9331
9332 #[tokio::test]
9333 async fn submit_order_denies_when_not_connected() {
9334 let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9335 let order = test_market_sell_order(test_pool().instrument_id);
9336 let mut receiver = start_with_events(&mut client);
9337
9338 client.submit_order(submit_order_cmd(&order)).unwrap();
9339
9340 let events = collect_order_events(&mut receiver);
9341 assert_eq!(events.len(), 1);
9342 let OrderEventAny::Denied(denied) = &events[0] else {
9343 panic!("expected OrderDenied, was {:?}", events[0]);
9344 };
9345 assert!(
9346 denied.reason.contains("is not connected"),
9347 "was: {}",
9348 denied.reason
9349 );
9350 }
9351
9352 #[tokio::test]
9353 async fn submit_order_denies_without_durable_store() {
9354 let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9355 client.core.set_connected();
9356 let order = test_market_sell_order(test_pool().instrument_id);
9357 let mut receiver = start_with_events(&mut client);
9358
9359 client.submit_order(submit_order_cmd(&order)).unwrap();
9360
9361 let events = collect_order_events(&mut receiver);
9362 assert_eq!(events.len(), 1);
9363 let OrderEventAny::Denied(denied) = &events[0] else {
9364 panic!("expected OrderDenied, was {:?}", events[0]);
9365 };
9366 assert!(
9367 denied.reason.contains("No durable store configured"),
9368 "was: {}",
9369 denied.reason
9370 );
9371 }
9372
9373 #[tokio::test]
9374 async fn submit_order_denies_when_transaction_in_flight() {
9375 let (mut client, _) = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string()));
9376 client.core.set_connected();
9377 *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
9378 intent_id: 1,
9379 nonce: 7,
9380 tx_hash: B256::ZERO,
9381 purpose: TransactionPurpose::Wrap,
9382 }));
9383 let order = test_market_sell_order(test_pool().instrument_id);
9384 let mut receiver = start_with_events(&mut client);
9385
9386 client.submit_order(submit_order_cmd(&order)).unwrap();
9387
9388 let events = collect_order_events(&mut receiver);
9389 assert_eq!(events.len(), 1);
9390 let OrderEventAny::Denied(denied) = &events[0] else {
9391 panic!("expected OrderDenied, was {:?}", events[0]);
9392 };
9393 assert!(
9394 denied.reason.contains("still awaiting finality"),
9395 "was: {}",
9396 denied.reason
9397 );
9398 }
9399
9400 #[tokio::test]
9401 async fn submit_order_denies_without_signer() {
9402 let Some((admin_pool, schema, mut client, _, _)) =
9403 swap_client_with_database("execution_submit_no_signer_test", swap_rpc_state().await)
9404 .await
9405 else {
9406 return;
9407 };
9408 client.signer = None;
9409 let order = test_market_sell_order(test_pool().instrument_id);
9410 let mut receiver = start_with_events(&mut client);
9411
9412 client.submit_order(submit_order_cmd(&order)).unwrap();
9413
9414 let events = collect_order_events(&mut receiver);
9415 assert_eq!(events.len(), 1);
9416 let OrderEventAny::Denied(denied) = &events[0] else {
9417 panic!("expected OrderDenied, was {:?}", events[0]);
9418 };
9419 assert!(
9420 denied.reason.contains("Signer not initialized"),
9421 "was: {}",
9422 denied.reason
9423 );
9424
9425 drop_execution_schema(&admin_pool, &schema).await;
9426 }
9427
9428 #[tokio::test]
9429 async fn submit_order_broadcasts_swap_and_records_client_order_id() {
9430 let Some((admin_pool, schema, mut client, state, _)) =
9431 swap_client_with_database("execution_submit_success_test", swap_rpc_state().await)
9432 .await
9433 else {
9434 return;
9435 };
9436 let order = test_market_sell_order(test_pool().instrument_id);
9437 let mut receiver = start_with_events(&mut client);
9438 let expected_min_out = expected_min_amount_out(50);
9439
9440 client.submit_order(submit_order_cmd(&order)).unwrap();
9441 await_pending_tasks(&client).await;
9442
9443 let events = collect_order_events(&mut receiver);
9444 assert_swap_submitted_and_filled(&events);
9445 let OrderEventAny::Submitted(submitted) = &events[0] else {
9446 panic!("expected OrderSubmitted, was {:?}", events[0]);
9447 };
9448 assert_eq!(submitted.client_order_id, order.client_order_id());
9449
9450 let (expected_hash, expected_raw) = expected_swap_tx(expected_min_out).await;
9452 let broadcasts: Vec<_> = state
9453 .recorded_requests()
9454 .into_iter()
9455 .filter(|request| request["method"] == "eth_sendRawTransaction")
9456 .collect();
9457 assert_eq!(broadcasts.len(), 1);
9458 assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
9459
9460 let record = client
9461 .cache
9462 .get_execution_transaction(42161, &expected_hash.to_string())
9463 .await
9464 .unwrap()
9465 .unwrap();
9466 assert_eq!(record.nonce, 7);
9467 assert_eq!(record.purpose, "swap");
9468 assert_eq!(record.status, "finalized");
9469 assert_eq!(
9470 record.client_order_id.as_deref(),
9471 Some(order.client_order_id().as_str())
9472 );
9473 assert!(client.in_flight.lock().is_none());
9474 let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
9475 "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
9476 )))
9477 .fetch_one(&admin_pool)
9478 .await
9479 .unwrap();
9480 let decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9481 "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9482 WHERE outcome = 'verified'"
9483 )))
9484 .fetch_one(&admin_pool)
9485 .await
9486 .unwrap();
9487 let connect_decision_count = sqlx::query_scalar::<_, i64>(sqlx::AssertSqlSafe(format!(
9488 "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
9489 WHERE decision_class = 'connect' AND outcome = 'verified'"
9490 )))
9491 .fetch_one(&admin_pool)
9492 .await
9493 .unwrap();
9494 assert_eq!(nonce_state, (8, 1));
9495 assert_eq!(decision_count, 35);
9496 assert_eq!(connect_decision_count, 1);
9497
9498 drop_execution_schema(&admin_pool, &schema).await;
9499 }
9500
9501 #[tokio::test]
9502 async fn submit_order_pins_every_pre_sign_state_read_to_swap_anchor() {
9503 let Some((admin_pool, schema, mut client, state, _)) =
9504 swap_client_with_database("execution_submit_anchor_reads_test", swap_rpc_state().await)
9505 .await
9506 else {
9507 return;
9508 };
9509 let order = test_market_sell_order(test_pool().instrument_id);
9510 let mut receiver = start_with_events(&mut client);
9511
9512 client.submit_order(submit_order_cmd(&order)).unwrap();
9513 await_pending_tasks(&client).await;
9514
9515 let events = collect_order_events(&mut receiver);
9516 assert_swap_submitted_and_filled(&events);
9517 let requests = state.recorded_requests();
9518 let broadcast_index = requests
9519 .iter()
9520 .position(|request| request["method"] == "eth_sendRawTransaction")
9521 .unwrap();
9522 let pre_broadcast = &requests[..broadcast_index];
9523 let latest_reads: Vec<_> = pre_broadcast
9524 .iter()
9525 .filter(|request| {
9526 request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9527 })
9528 .collect();
9529 assert_eq!(latest_reads.len(), 3);
9530 assert!(
9531 latest_reads
9532 .iter()
9533 .all(|request| request["params"] == serde_json::json!(["latest", false]))
9534 );
9535
9536 for method in [
9537 "eth_getCode",
9538 "eth_call",
9539 "eth_estimateGas",
9540 "eth_getBalance",
9541 ] {
9542 let pinned: Vec<_> = pre_broadcast
9543 .iter()
9544 .filter(|request| request["method"] == method)
9545 .collect();
9546 assert!(!pinned.is_empty(), "method {method}");
9547 assert_eq!(pinned.len() % 3, 0, "method {method}: {pinned:?}");
9548 assert!(
9549 pinned
9550 .iter()
9551 .all(|request| request["params"][1] == FIXTURE_BLOCK_PARAM),
9552 "method {method}: {pinned:?}"
9553 );
9554 }
9555
9556 let numbered_blocks: Vec<_> = pre_broadcast
9557 .iter()
9558 .filter(|request| {
9559 request["method"] == "eth_getBlockByNumber"
9560 && request["params"][0] == FIXTURE_BLOCK_PARAM
9561 })
9562 .collect();
9563 assert!(!numbered_blocks.is_empty());
9564 assert_eq!(numbered_blocks.len() % 3, 0);
9565 assert!(
9566 numbered_blocks
9567 .iter()
9568 .all(|request| request["params"][1] == false)
9569 );
9570
9571 let chain_ids: Vec<_> = pre_broadcast
9572 .iter()
9573 .filter(|request| request["method"] == "eth_chainId")
9574 .collect();
9575 assert_eq!(chain_ids.len(), 3);
9576 assert!(
9577 chain_ids
9578 .iter()
9579 .all(|request| request["params"] == serde_json::json!([]))
9580 );
9581 let nonces: Vec<_> = pre_broadcast
9582 .iter()
9583 .filter(|request| request["method"] == "eth_getTransactionCount")
9584 .collect();
9585 assert_eq!(nonces.len(), 6);
9586 assert_eq!(
9587 nonces
9588 .iter()
9589 .filter(|request| {
9590 request["params"] == serde_json::json!([WALLET.to_ascii_lowercase(), "pending"])
9591 })
9592 .count(),
9593 3
9594 );
9595 assert_eq!(
9596 nonces
9597 .iter()
9598 .filter(|request| {
9599 request["params"]
9600 == serde_json::json!([WALLET.to_ascii_lowercase(), FIXTURE_BLOCK_PARAM])
9601 })
9602 .count(),
9603 3
9604 );
9605 let priority_fees: Vec<_> = pre_broadcast
9606 .iter()
9607 .filter(|request| request["method"] == "eth_maxPriorityFeePerGas")
9608 .collect();
9609 assert_eq!(priority_fees.len(), 3);
9610 assert!(
9611 priority_fees
9612 .iter()
9613 .all(|request| request["params"] == serde_json::json!([]))
9614 );
9615
9616 let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
9617 assert_eq!(
9618 requests[broadcast_index]["params"][0].as_str().unwrap(),
9619 expected_raw
9620 );
9621 let created_block: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9622 "SELECT created_block FROM {schema}.execution_intent"
9623 )))
9624 .fetch_one(&admin_pool)
9625 .await
9626 .unwrap();
9627 assert_eq!(created_block, i64::try_from(FIXTURE_BLOCK).unwrap());
9628
9629 drop_execution_schema(&admin_pool, &schema).await;
9630 }
9631
9632 #[tokio::test]
9633 async fn submit_order_denies_changed_swap_anchor_before_signing() {
9634 let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9635 let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9636 let state = swap_rpc_state().await.with_parameter_response_sequence(
9637 "eth_getBlockByNumber",
9638 FIXTURE_BLOCK_PARAM,
9639 &[
9640 &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9641 &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9642 &canonical, &changed, &changed, &changed,
9643 ],
9644 );
9645 let Some((admin_pool, schema, mut client, state, _)) =
9646 swap_client_with_database("execution_submit_anchor_change_test", state).await
9647 else {
9648 return;
9649 };
9650 let order = test_market_sell_order(test_pool().instrument_id);
9651 let mut receiver = start_with_events(&mut client);
9652
9653 client.submit_order(submit_order_cmd(&order)).unwrap();
9654 await_pending_tasks(&client).await;
9655
9656 let events = collect_order_events(&mut receiver);
9657 assert_eq!(events.len(), 1, "was: {events:?}");
9658 let OrderEventAny::Denied(denied) = &events[0] else {
9659 panic!("expected OrderDenied, was {:?}", events[0]);
9660 };
9661 assert!(
9662 denied
9663 .reason
9664 .contains("pre-sign checkpoint reread verification disagreed"),
9665 "was: {}",
9666 denied.reason
9667 );
9668 assert_eq!(
9669 execution_intent_markers(&admin_pool, &schema).await,
9670 vec![("swap".into(), "recoverable".into(), false, false)]
9671 );
9672 let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9673 "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9674 )))
9675 .fetch_one(&admin_pool)
9676 .await
9677 .unwrap();
9678 assert_eq!(signed_count, 0);
9679 assert!(
9680 state
9681 .recorded_requests()
9682 .iter()
9683 .all(|request| { request["method"] != "eth_sendRawTransaction" })
9684 );
9685 assert!(client.in_flight.lock().is_none());
9686
9687 drop_execution_schema(&admin_pool, &schema).await;
9688 }
9689
9690 #[tokio::test]
9691 async fn submit_order_keeps_verified_swap_anchor_when_latest_advances() {
9692 let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9693 let newer = fixture_block_response(FIXTURE_BLOCK + 1, B256::from([0x44; 32]));
9694 let state = swap_rpc_state().await.with_parameter_response_sequence(
9695 "eth_getBlockByNumber",
9696 "latest",
9697 &[&canonical, &canonical, &canonical, &newer, &newer, &newer],
9698 );
9699 let Some((admin_pool, schema, mut client, state, _)) =
9700 swap_client_with_database("execution_submit_advancing_head_test", state).await
9701 else {
9702 return;
9703 };
9704 let order = test_market_sell_order(test_pool().instrument_id);
9705 let mut receiver = start_with_events(&mut client);
9706
9707 client.submit_order(submit_order_cmd(&order)).unwrap();
9708 await_pending_tasks(&client).await;
9709
9710 let events = collect_order_events(&mut receiver);
9711 assert_swap_submitted_and_filled(&events);
9712 let latest_reads = state
9713 .recorded_requests()
9714 .iter()
9715 .filter(|request| {
9716 request["method"] == "eth_getBlockByNumber" && request["params"][0] == "latest"
9717 })
9718 .count();
9719 assert_eq!(latest_reads, 3);
9720
9721 drop_execution_schema(&admin_pool, &schema).await;
9722 }
9723
9724 #[tokio::test]
9725 async fn submit_order_denies_changed_quote_watermark_before_signing() {
9726 let watermark_number = FIXTURE_BLOCK;
9727 let watermark_hash = B256::from([0x11; 32]);
9728 let canonical = fixture_block_response(watermark_number, watermark_hash);
9729 let changed = fixture_block_response(watermark_number, B256::from([0x66; 32]));
9730 let watermark_param = format!("0x{watermark_number:x}");
9731 let min_amount_out = expected_min_amount_out(50);
9732 let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
9733 let head = finalized_swap_block(tx_hash, min_amount_out);
9734 let state = swap_rpc_state()
9735 .await
9736 .with_response("eth_getBlockByNumber", &head)
9737 .with_parameter_response_sequence(
9738 "eth_getBlockByNumber",
9739 &watermark_param,
9740 &[
9741 &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9742 &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9743 &changed, &changed, &changed,
9744 ],
9745 );
9746 let Some((admin_pool, schema, mut client, state, cache)) =
9747 swap_client_with_database("execution_submit_watermark_change_test", state).await
9748 else {
9749 return;
9750 };
9751 let pool = test_pool();
9752 cache
9753 .borrow_mut()
9754 .add_pool_profiler(test_profiler_at_block(
9755 &pool,
9756 watermark_number,
9757 &watermark_hash.to_string(),
9758 ))
9759 .unwrap();
9760 let order = test_market_sell_order(pool.instrument_id);
9761 let mut receiver = start_with_events(&mut client);
9762
9763 client.submit_order(submit_order_cmd(&order)).unwrap();
9764 await_pending_tasks(&client).await;
9765
9766 let events = collect_order_events(&mut receiver);
9767 assert_eq!(events.len(), 1, "was: {events:?}");
9768 let OrderEventAny::Denied(denied) = &events[0] else {
9769 panic!("expected OrderDenied, was {:?}", events[0]);
9770 };
9771 assert!(
9772 denied.reason.contains("changed before signing"),
9773 "was: {}",
9774 denied.reason
9775 );
9776 assert_eq!(
9777 execution_intent_markers(&admin_pool, &schema).await,
9778 vec![("swap".into(), "recoverable".into(), false, false)]
9779 );
9780 let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9781 "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9782 )))
9783 .fetch_one(&admin_pool)
9784 .await
9785 .unwrap();
9786 assert_eq!(signed_count, 0);
9787 assert!(
9788 state
9789 .recorded_requests()
9790 .iter()
9791 .all(|request| { request["method"] != "eth_sendRawTransaction" })
9792 );
9793 assert!(client.in_flight.lock().is_none());
9794
9795 drop_execution_schema(&admin_pool, &schema).await;
9796 }
9797
9798 #[tokio::test]
9799 async fn changed_swap_anchor_retains_ownership_when_recovery_commit_fails() {
9800 let canonical = fixture_block_response(FIXTURE_BLOCK, B256::from([0x11; 32]));
9801 let changed = fixture_block_response(FIXTURE_BLOCK, B256::from([0x44; 32]));
9802 let state = swap_rpc_state().await.with_parameter_response_sequence(
9803 "eth_getBlockByNumber",
9804 FIXTURE_BLOCK_PARAM,
9805 &[
9806 &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9807 &canonical, &canonical, &canonical, &canonical, &canonical, &canonical, &canonical,
9808 &canonical, &changed, &changed, &changed,
9809 ],
9810 );
9811 let Some((admin_pool, schema, mut client, state, _)) =
9812 swap_client_with_database("execution_submit_anchor_recovery_fail_test", state).await
9813 else {
9814 return;
9815 };
9816 install_recoverable_commit_rejection(&admin_pool, &schema).await;
9817 let order = test_market_sell_order(test_pool().instrument_id);
9818 let mut receiver = start_with_events(&mut client);
9819
9820 client.submit_order(submit_order_cmd(&order)).unwrap();
9821 await_pending_tasks(&client).await;
9822
9823 let events = collect_order_events(&mut receiver);
9824 assert_eq!(events.len(), 1, "was: {events:?}");
9825 let OrderEventAny::Denied(denied) = &events[0] else {
9826 panic!("expected OrderDenied, was {:?}", events[0]);
9827 };
9828 assert!(
9829 denied
9830 .reason
9831 .contains("pre-sign checkpoint reread verification disagreed"),
9832 "was: {}",
9833 denied.reason
9834 );
9835 assert_eq!(
9836 execution_intent_markers(&admin_pool, &schema).await,
9837 vec![("swap".into(), "prepared".into(), false, true)]
9838 );
9839 let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
9840 "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
9841 )))
9842 .fetch_one(&admin_pool)
9843 .await
9844 .unwrap();
9845 assert_eq!(signed_count, 0);
9846 assert!(
9847 state
9848 .recorded_requests()
9849 .iter()
9850 .all(|request| { request["method"] != "eth_sendRawTransaction" })
9851 );
9852 assert!(matches!(
9853 *client.in_flight.lock(),
9854 Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
9855 ));
9856
9857 drop_execution_schema(&admin_pool, &schema).await;
9858 }
9859
9860 #[rstest]
9861 #[case::filled("execution_inflight_finality_fill_test", false)]
9862 #[case::reverted("execution_inflight_finality_revert_test", true)]
9863 #[tokio::test]
9864 async fn swap_awaiting_finality_survives_inflight_exhaustion(
9865 #[case] test_name: &str,
9866 #[case] reverted: bool,
9867 ) {
9868 let min_amount_out = expected_min_amount_out(50);
9869 let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
9870
9871 let state = if reverted {
9872 let block = finalized_swap_block(expected_hash, min_amount_out);
9873 let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
9874 with_finalized_identity(
9875 swap_rpc_state()
9876 .await
9877 .with_response("eth_getTransactionReceipt", &receipt),
9878 &block,
9879 &receipt,
9880 )
9881 } else {
9882 finalized_swap_rpc_state(expected_hash, min_amount_out)
9883 };
9884
9885 let Some((admin_pool, schema, mut client, _state, cache)) =
9886 swap_client_with_database(test_name, state).await
9887 else {
9888 return;
9889 };
9890
9891 let order = test_market_sell_order(test_pool().instrument_id);
9892 let client_order_id = order.client_order_id();
9893 let mut receiver = start_with_events(&mut client);
9894
9895 let plan = client
9896 .prepare_swap(&submit_order_cmd(&order), &order)
9897 .unwrap();
9898 execute_swap(
9899 plan,
9900 client.transaction_executor().unwrap(),
9901 client.emitter.clone(),
9902 client.transaction_limits.max_quote_age_blocks,
9903 client.transaction_limits.deadline_seconds,
9904 )
9905 .await
9906 .unwrap();
9907 let swap_events = collect_order_events(&mut receiver);
9908 assert_eq!(swap_events.len(), 2, "was: {swap_events:?}");
9909 assert!(matches!(&swap_events[0], OrderEventAny::Submitted(_)));
9910
9911 let clock: Rc<RefCell<dyn Clock>> = Rc::new(RefCell::new(VirtualClock::new()));
9912 let config = ExecutionManagerConfig::default();
9913 let max_retries = config.inflight_max_retries;
9914 let mut manager = ExecutionManager::new(clock, Rc::clone(&cache), config).unwrap();
9915
9916 assert!(manager.check_open_orders(&[&client]).await.is_empty());
9918 cache.borrow_mut().update_order(&swap_events[0]).unwrap();
9919 tokio::time::pause();
9920 manager.register_submission(order.init_event(), Some(client.client_id()));
9921
9922 let mut inflight_events = Vec::new();
9924 let mut queries_sent = 0;
9925
9926 for _ in 0..60 {
9927 tokio::time::advance(Duration::from_secs(1)).await;
9928 let result = manager.check_inflight_orders();
9929 inflight_events.extend(result.events);
9930
9931 for query in result.queries {
9932 let TradingCommand::QueryOrder(query) = query else {
9933 panic!("expected QueryOrder, was {query:?}");
9934 };
9935
9936 client.query_order(query).unwrap();
9937 queries_sent += 1;
9938 }
9939 }
9940
9941 let exhaustions = manager.take_submission_recovery_exhaustions();
9942 let status_before_finality = cache.borrow().order(&client_order_id).unwrap().status();
9943 cache.borrow_mut().update_order(&swap_events[1]).unwrap();
9944 tokio::time::resume();
9945 drop_execution_schema(&admin_pool, &schema).await;
9946
9947 assert!(inflight_events.is_empty(), "was: {inflight_events:?}");
9948 assert_eq!(queries_sent, max_retries - 1);
9949 assert!(collect_order_events(&mut receiver).is_empty());
9950 assert_eq!(
9951 exhaustions,
9952 vec![SubmissionRecoveryExhausted {
9953 trader_id: order.trader_id(),
9954 client_id: Some(client.client_id()),
9955 strategy_id: order.strategy_id(),
9956 instrument_id: order.instrument_id(),
9957 client_order_id,
9958 source: SubmissionRecoverySource::Inflight,
9959 retry_count: max_retries,
9960 ts_event: UnixNanos::default(),
9961 }]
9962 );
9963 assert_eq!(status_before_finality, OrderStatus::Submitted);
9964 let cache = cache.borrow();
9965 let resolved = cache.order(&client_order_id).unwrap();
9966
9967 if reverted {
9968 let OrderEventAny::Rejected(rejected) = &swap_events[1] else {
9969 panic!("expected OrderRejected, was {:?}", swap_events[1]);
9970 };
9971
9972 assert_eq!(
9973 rejected.reason.as_str(),
9974 format!("Transaction {expected_hash} reverted on-chain")
9975 );
9976 assert!(!rejected.reconciliation);
9977 assert_eq!(resolved.status(), OrderStatus::Rejected);
9978 assert_eq!(
9979 resolved.filled_qty(),
9980 Quantity::zero(order.quantity().precision)
9981 );
9982 } else {
9983 assert!(matches!(&swap_events[1], OrderEventAny::Filled(_)));
9984 assert_eq!(resolved.status(), OrderStatus::Filled);
9985 assert_eq!(resolved.filled_qty(), order.quantity());
9986 }
9987 }
9988
9989 #[tokio::test]
9990 async fn finalized_swap_emits_exact_fill_once_and_refreshes_wallet() {
9991 let min_amount_out = expected_min_amount_out(50);
9992 let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
9993 let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
9994 let Some((admin_pool, schema, mut client, state, _)) =
9995 swap_client_with_database("execution_submit_fill_test", state).await
9996 else {
9997 return;
9998 };
9999 let order = test_market_sell_order(test_pool().instrument_id);
10000 let mut receiver = start_with_events(&mut client);
10001
10002 let plan = client
10003 .prepare_swap(&submit_order_cmd(&order), &order)
10004 .unwrap();
10005 execute_swap(
10006 plan,
10007 client.transaction_executor().unwrap(),
10008 client.emitter.clone(),
10009 client.transaction_limits.max_quote_age_blocks,
10010 client.transaction_limits.deadline_seconds,
10011 )
10012 .await
10013 .unwrap();
10014
10015 let (nonce, wallet_address, transaction_to, transaction_input, transaction_value): (
10016 i64,
10017 String,
10018 String,
10019 String,
10020 String,
10021 ) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10022 "SELECT nonce, wallet_address, transaction_to, transaction_input, transaction_value \
10023 FROM {schema}.execution_intent"
10024 )))
10025 .fetch_one(&admin_pool)
10026 .await
10027 .unwrap();
10028 let finalized_block = client
10029 .http_rpc_client
10030 .block_by_number(FIXTURE_BLOCK + 1, true)
10031 .await
10032 .unwrap();
10033 let finalized_transaction = finalized_block
10034 .transactions
10035 .iter()
10036 .find(|transaction| transaction.hash == expected_hash)
10037 .unwrap();
10038 assert_eq!(finalized_transaction.from.to_string(), wallet_address);
10039 assert_eq!(finalized_transaction.nonce, u64::try_from(nonce).unwrap());
10040 assert_eq!(
10041 finalized_transaction.to,
10042 Some(Address::from_str(&transaction_to).unwrap())
10043 );
10044 assert_eq!(
10045 finalized_transaction.input.as_ref(),
10046 hex::decode(transaction_input.strip_prefix("0x").unwrap()).unwrap()
10047 );
10048 assert_eq!(
10049 finalized_transaction.value,
10050 U256::from_str(&transaction_value).unwrap()
10051 );
10052
10053 let mut order_events = Vec::new();
10054 let mut account_states = Vec::new();
10055
10056 while let Ok(event) = receiver.try_recv() {
10057 match event {
10058 ExecutionEvent::Order(event) => order_events.push(event),
10059 ExecutionEvent::Account(state) => account_states.push(state),
10060 other => panic!("unexpected execution event: {other:?}"),
10061 }
10062 }
10063 assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10064 assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10065 let OrderEventAny::Filled(fill) = &order_events[1] else {
10066 panic!("expected OrderFilled, was {:?}", order_events[1]);
10067 };
10068 let expected_commission = Money::from_u256(
10069 U256::from(50_112_u64) * U256::from(100_000_000_u64),
10070 test_pool().chain.native_currency(),
10071 )
10072 .unwrap();
10073 assert_eq!(fill.client_order_id, order.client_order_id());
10074 assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10075 assert_eq!(fill.order_side, OrderSide::Sell);
10076 assert_eq!(fill.last_qty, Quantity::from("0.001"));
10077 assert_eq!(fill.last_px, Price::from("1000"));
10078 assert_eq!(fill.currency.code, "USDC");
10079 assert_eq!(fill.commission, Some(expected_commission));
10080 assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10081 assert_eq!(account_states.len(), 1);
10082 let account_state = &account_states[0];
10083 assert_eq!(account_state.account_id, AccountId::from("BLOCKCHAIN-001"));
10084 assert_eq!(account_state.account_type, AccountType::Wallet);
10085 assert_eq!(account_state.base_currency, None);
10086 assert_eq!(account_state.balances.len(), 3);
10087 assert!(account_state.margins.is_empty());
10088 assert!(account_state.is_reported);
10089 assert_eq!(
10090 account_state.balances,
10091 client.wallet_balance.lock().as_account_balances().unwrap()
10092 );
10093
10094 let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10095 "SELECT fill_emitted, active FROM {schema}.execution_intent"
10096 )))
10097 .fetch_one(&admin_pool)
10098 .await
10099 .unwrap();
10100 assert!(fill_emitted);
10101 assert!(!active);
10102
10103 let database = client.cache.database.as_ref().unwrap().clone();
10104 let payload_keys = client.payload_keys.clone();
10105 let restart_config = client.config.clone();
10106 drop(client);
10107 let (mut restarted, _) = swap_client_with_cache(restart_config);
10108 restarted.cache.database = Some(database);
10109 restarted.payload_keys = payload_keys;
10110 restarted.signer = Some(Arc::new(
10111 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10112 ));
10113 let mut restart_receiver = start_with_events(&mut restarted);
10114 restarted.reconcile_unresolved_execution().await.unwrap();
10115 restarted.reconcile_unresolved_execution().await.unwrap();
10116 assert!(collect_order_events(&mut restart_receiver).is_empty());
10117 let requests = state.recorded_requests();
10118 assert_eq!(
10119 requests
10120 .iter()
10121 .filter(|request| request["method"] == "eth_sendRawTransaction")
10122 .count(),
10123 1
10124 );
10125 assert_eq!(
10126 requests
10127 .iter()
10128 .filter(|request| {
10129 request["method"] == "eth_call"
10130 && request["params"][0]["data"]
10131 .as_str()
10132 .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
10133 })
10134 .count(),
10135 9
10136 );
10137 assert_eq!(
10138 requests
10139 .iter()
10140 .filter(|request| request["method"] == "eth_getBalance")
10141 .count(),
10142 6
10143 );
10144
10145 drop_execution_schema(&admin_pool, &schema).await;
10146 }
10147
10148 #[tokio::test]
10149 async fn restart_emits_committed_swap_from_verified_inclusion_header() {
10150 let min_amount_out = expected_min_amount_out(50);
10151 let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10152 let state = finalized_swap_rpc_state(expected_hash, min_amount_out);
10153 let Some((admin_pool, schema, mut client, _, _)) =
10154 swap_client_with_database("execution_committed_fill_restart_test", state).await
10155 else {
10156 return;
10157 };
10158 let order = test_market_sell_order(test_pool().instrument_id);
10159 let mut receiver = start_with_events(&mut client);
10160 let plan = client
10161 .prepare_swap(&submit_order_cmd(&order), &order)
10162 .unwrap();
10163
10164 execute_swap(
10165 plan,
10166 client.transaction_executor().unwrap(),
10167 client.emitter.clone(),
10168 client.transaction_limits.max_quote_age_blocks,
10169 client.transaction_limits.deadline_seconds,
10170 )
10171 .await
10172 .unwrap();
10173 assert_swap_submitted_and_filled(&collect_order_events(&mut receiver));
10174 let ancestry_range: (i64, i64) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10175 "SELECT height_start, height_end FROM {schema}.execution_verification_decision \
10176 WHERE decision_class = 'finality' AND read_class = 'numbered_block' \
10177 AND height_end > height_start"
10178 )))
10179 .fetch_one(&admin_pool)
10180 .await
10181 .unwrap();
10182 assert_eq!(
10183 ancestry_range,
10184 ((FIXTURE_BLOCK + 1) as i64, (FIXTURE_BLOCK + 2) as i64)
10185 );
10186
10187 sqlx::query(sqlx::AssertSqlSafe(format!(
10188 "UPDATE {schema}.execution_intent SET fill_emitted = FALSE, active = TRUE"
10189 )))
10190 .execute(&admin_pool)
10191 .await
10192 .unwrap();
10193 let database = client.cache.database.as_ref().unwrap().clone();
10194 let payload_keys = client.payload_keys.clone();
10195 let restart_config = client.config.clone();
10196 drop(client);
10197 let (mut restarted, _) = swap_client_with_cache(restart_config);
10198 restarted.cache.database = Some(database);
10199 restarted.payload_keys = payload_keys;
10200 restarted.signer = Some(Arc::new(
10201 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10202 ));
10203 let mut restart_receiver = start_with_events(&mut restarted);
10204
10205 restarted.reconcile_unresolved_execution().await.unwrap();
10206
10207 let events = collect_order_events(&mut restart_receiver);
10208 let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10209 "SELECT fill_emitted, active FROM {schema}.execution_intent"
10210 )))
10211 .fetch_one(&admin_pool)
10212 .await
10213 .unwrap();
10214 assert_eq!(events.len(), 1, "was: {events:?}");
10215 assert!(matches!(&events[0], OrderEventAny::Filled(_)));
10216 assert!(fill_emitted);
10217 assert!(!active);
10218
10219 drop_execution_schema(&admin_pool, &schema).await;
10220 }
10221
10222 #[tokio::test]
10223 async fn finalized_swap_ignores_unrelated_swap_logs() {
10224 let min_amount_out = expected_min_amount_out(50);
10225 let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10226 let receipt = finalized_swap_receipt_with_unrelated_swap(expected_hash);
10227 let state = finalized_swap_rpc_state(expected_hash, min_amount_out)
10228 .with_response("eth_getTransactionReceipt", &receipt);
10229 let Some((admin_pool, schema, mut client, _, _)) =
10230 swap_client_with_database("execution_submit_unrelated_log_test", state).await
10231 else {
10232 return;
10233 };
10234 let order = test_market_sell_order(test_pool().instrument_id);
10235 let mut receiver = start_with_events(&mut client);
10236
10237 client.submit_order(submit_order_cmd(&order)).unwrap();
10238 await_pending_tasks(&client).await;
10239
10240 let events = collect_order_events(&mut receiver);
10241 assert_swap_submitted_and_filled(&events);
10242
10243 drop_execution_schema(&admin_pool, &schema).await;
10244 }
10245
10246 #[tokio::test]
10247 async fn prepare_swap_buy_accepts_quote_spend_exact_boundary() {
10248 let amount_in = expected_buy_amount_in();
10249 let min_amount_out = expected_buy_min_amount_out(50);
10250 let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10251 let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10252 let max_amount = amount_in.to_string();
10253 let Some((admin_pool, schema, client, _, cache)) = swap_client_with_database_config(
10254 "execution_prepare_buy_test",
10255 state,
10256 move |http_rpc_url| {
10257 let mut config = buy_test_config(http_rpc_url);
10258 config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount;
10259 config
10260 },
10261 )
10262 .await
10263 else {
10264 return;
10265 };
10266 let order = test_market_buy_order(test_pool().instrument_id);
10267 cache
10268 .borrow_mut()
10269 .add_order(order.clone(), None, None, true)
10270 .unwrap();
10271
10272 let plan = client
10273 .prepare_swap(&submit_order_cmd(&order), &order)
10274 .unwrap();
10275
10276 assert_eq!(plan.token_in, USDC_ADDRESS);
10277 assert_eq!(plan.token_out, WETH_ADDRESS);
10278 assert_eq!(plan.amount_in, amount_in);
10279 assert_eq!(plan.min_amount_out, min_amount_out);
10280 assert_ne!(plan.token_in, WETH_ADDRESS);
10281
10282 drop_execution_schema(&admin_pool, &schema).await;
10283 }
10284
10285 #[tokio::test]
10286 async fn submit_order_broadcasts_buy_swap() {
10287 let amount_in = expected_buy_amount_in();
10288 let min_amount_out = expected_buy_min_amount_out(50);
10289 let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10290 let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10291 let Some((admin_pool, schema, mut client, state, cache)) =
10292 swap_client_with_buy_database("execution_submit_buy_success_test", state).await
10293 else {
10294 return;
10295 };
10296 let order = test_market_buy_order(test_pool().instrument_id);
10297 cache
10298 .borrow_mut()
10299 .add_order(order.clone(), None, None, true)
10300 .unwrap();
10301 let mut receiver = start_with_events(&mut client);
10302
10303 client.submit_order(submit_order_cmd(&order)).unwrap();
10304 await_pending_tasks(&client).await;
10305
10306 let events = collect_order_events(&mut receiver);
10307 assert_swap_submitted_and_filled(&events);
10308 let broadcasts: Vec<_> = state
10309 .recorded_requests()
10310 .into_iter()
10311 .filter(|request| request["method"] == "eth_sendRawTransaction")
10312 .collect();
10313 assert_eq!(broadcasts.len(), 1);
10314 assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10315
10316 drop_execution_schema(&admin_pool, &schema).await;
10317 }
10318
10319 #[tokio::test]
10320 async fn finalized_buy_swap_emits_fill_from_output_leg() {
10321 let amount_in = expected_buy_amount_in();
10322 let min_amount_out = expected_buy_min_amount_out(50);
10323 let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10324 let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10325 let Some((admin_pool, schema, mut client, state, cache)) =
10326 swap_client_with_buy_database("execution_submit_buy_fill_test", state).await
10327 else {
10328 return;
10329 };
10330 let order = test_market_buy_order(test_pool().instrument_id);
10331 cache
10332 .borrow_mut()
10333 .add_order(order.clone(), None, None, true)
10334 .unwrap();
10335 let mut receiver = start_with_events(&mut client);
10336 let plan = client
10337 .prepare_swap(&submit_order_cmd(&order), &order)
10338 .unwrap();
10339
10340 execute_swap(
10341 plan,
10342 client.transaction_executor().unwrap(),
10343 client.emitter.clone(),
10344 client.transaction_limits.max_quote_age_blocks,
10345 client.transaction_limits.deadline_seconds,
10346 )
10347 .await
10348 .unwrap();
10349
10350 let mut order_events = Vec::new();
10351
10352 while let Ok(event) = receiver.try_recv() {
10353 if let ExecutionEvent::Order(event) = event {
10354 order_events.push(event);
10355 }
10356 }
10357 assert_eq!(order_events.len(), 2, "was: {order_events:?}");
10358 assert!(matches!(&order_events[0], OrderEventAny::Submitted(_)));
10359 let OrderEventAny::Filled(fill) = &order_events[1] else {
10360 panic!("expected OrderFilled, was {:?}", order_events[1]);
10361 };
10362 let expected_commission = Money::from_u256(
10363 U256::from(50_112_u64) * U256::from(100_000_000_u64),
10364 test_pool().chain.native_currency(),
10365 )
10366 .unwrap();
10367 assert_eq!(fill.client_order_id, order.client_order_id());
10368 assert_eq!(fill.venue_order_id.as_str(), expected_hash.to_string());
10369 assert_eq!(fill.order_side, OrderSide::Buy);
10370 assert_eq!(fill.last_qty, Quantity::from("0.001"));
10371 assert_eq!(fill.currency.code, "USDC");
10372 assert_eq!(fill.commission, Some(expected_commission));
10373 assert_eq!(fill.liquidity_side, LiquiditySide::Taker);
10374 assert_eq!(
10375 Money::from_decimal(
10376 fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10377 fill.currency,
10378 )
10379 .unwrap(),
10380 Money::from_u256(amount_in, fill.currency).unwrap()
10381 );
10382
10383 let (fill_emitted, active): (bool, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
10384 "SELECT fill_emitted, active FROM {schema}.execution_intent"
10385 )))
10386 .fetch_one(&admin_pool)
10387 .await
10388 .unwrap();
10389 assert!(fill_emitted);
10390 assert!(!active);
10391
10392 let database = client.cache.database.as_ref().unwrap().clone();
10393 let payload_keys = client.payload_keys.clone();
10394 let restart_config = client.config.clone();
10395 drop(client);
10396 let (mut restarted, _) = swap_client_with_cache(restart_config);
10397 restarted.cache.database = Some(database);
10398 restarted.payload_keys = payload_keys;
10399 restarted.signer = Some(Arc::new(
10400 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
10401 ));
10402 let mut restart_receiver = start_with_events(&mut restarted);
10403 restarted.reconcile_unresolved_execution().await.unwrap();
10404 restarted.reconcile_unresolved_execution().await.unwrap();
10405 assert!(collect_order_events(&mut restart_receiver).is_empty());
10406 assert_eq!(
10407 state
10408 .recorded_requests()
10409 .iter()
10410 .filter(|request| request["method"] == "eth_sendRawTransaction")
10411 .count(),
10412 1
10413 );
10414
10415 drop_execution_schema(&admin_pool, &schema).await;
10416 }
10417
10418 #[tokio::test]
10419 async fn finalized_buy_swap_cancels_remainder_when_output_is_short() {
10420 let amount_in = expected_buy_amount_in();
10421 let min_amount_out = expected_buy_min_amount_out(50);
10422 let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10423 let short_base_out = min_amount_out;
10424 let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10425 state = state.with_response(
10426 "eth_getTransactionReceipt",
10427 &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, short_base_out),
10428 );
10429 let Some((admin_pool, schema, mut client, _, cache)) =
10430 swap_client_with_buy_database("execution_submit_buy_short_fill_test", state).await
10431 else {
10432 return;
10433 };
10434 let order = test_market_buy_order(test_pool().instrument_id);
10435 cache
10436 .borrow_mut()
10437 .add_order(order.clone(), None, None, true)
10438 .unwrap();
10439 let mut receiver = start_with_events(&mut client);
10440 let plan = client
10441 .prepare_swap(&submit_order_cmd(&order), &order)
10442 .unwrap();
10443
10444 execute_swap(
10445 plan,
10446 client.transaction_executor().unwrap(),
10447 client.emitter.clone(),
10448 client.transaction_limits.max_quote_age_blocks,
10449 client.transaction_limits.deadline_seconds,
10450 )
10451 .await
10452 .unwrap();
10453
10454 let events = collect_order_events(&mut receiver);
10455 assert_eq!(events.len(), 3, "was: {events:?}");
10456 assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10457 let OrderEventAny::Filled(fill) = &events[1] else {
10458 panic!("expected OrderFilled, was {:?}", events[1]);
10459 };
10460 let expected_qty = raw_amount_to_quantity(short_base_out, 18).unwrap();
10461 assert_eq!(fill.order_side, OrderSide::Buy);
10462 assert_eq!(fill.last_qty, expected_qty);
10463 assert!(fill.last_qty < order.quantity());
10464 let OrderEventAny::Canceled(canceled) = &events[2] else {
10465 panic!("expected OrderCanceled, was {:?}", events[2]);
10466 };
10467 assert_eq!(canceled.client_order_id, order.client_order_id());
10468 assert_eq!(
10469 canceled.venue_order_id.as_ref().map(VenueOrderId::as_str),
10470 Some(expected_hash.to_string().as_str())
10471 );
10472
10473 drop_execution_schema(&admin_pool, &schema).await;
10474 }
10475
10476 #[tokio::test]
10477 async fn finalized_buy_swap_reports_full_output_when_above_order_quantity() {
10478 let amount_in = expected_buy_amount_in();
10479 let min_amount_out = expected_buy_min_amount_out(50);
10480 let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10481 let overshoot_base_out = expected_buy_base_amount() * U256::from(2) + U256::from(44);
10482 let mut state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10483 state = state.with_response(
10484 "eth_getTransactionReceipt",
10485 &finalized_buy_swap_receipt_with_base_out(expected_hash, amount_in, overshoot_base_out),
10486 );
10487 let Some((admin_pool, schema, mut client, _, cache)) =
10488 swap_client_with_buy_database("execution_submit_buy_overshoot_fill_test", state).await
10489 else {
10490 return;
10491 };
10492 let order = test_market_buy_order(test_pool().instrument_id);
10493 cache
10494 .borrow_mut()
10495 .add_order(order.clone(), None, None, true)
10496 .unwrap();
10497 let mut receiver = start_with_events(&mut client);
10498 let plan = client
10499 .prepare_swap(&submit_order_cmd(&order), &order)
10500 .unwrap();
10501
10502 execute_swap(
10503 plan,
10504 client.transaction_executor().unwrap(),
10505 client.emitter.clone(),
10506 client.transaction_limits.max_quote_age_blocks,
10507 client.transaction_limits.deadline_seconds,
10508 )
10509 .await
10510 .unwrap();
10511
10512 let events = collect_order_events(&mut receiver);
10513 assert_eq!(events.len(), 2, "was: {events:?}");
10514 assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10515 let OrderEventAny::Filled(fill) = &events[1] else {
10516 panic!("expected OrderFilled, was {:?}", events[1]);
10517 };
10518 let expected_qty = raw_amount_to_quantity(overshoot_base_out, 18).unwrap();
10519 assert_eq!(fill.order_side, OrderSide::Buy);
10520 assert_eq!(fill.last_qty, expected_qty);
10521 assert!(fill.last_qty > order.quantity());
10522 assert_eq!(
10523 Money::from_decimal(
10524 fill.last_qty.as_decimal() * fill.last_px.as_decimal(),
10525 fill.currency,
10526 )
10527 .unwrap(),
10528 Money::from_u256(amount_in, fill.currency).unwrap()
10529 );
10530
10531 drop_execution_schema(&admin_pool, &schema).await;
10532 }
10533
10534 #[tokio::test]
10535 async fn finalized_buy_swap_quarantines_sell_oriented_log() {
10536 let amount_in = expected_buy_amount_in();
10537 let min_amount_out = expected_buy_min_amount_out(50);
10538 let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10539 let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10540 .with_response(
10541 "eth_getTransactionReceipt",
10542 &finalized_swap_receipt(expected_hash),
10543 );
10544 let Some((admin_pool, schema, mut client, _, cache)) =
10545 swap_client_with_buy_database("execution_submit_buy_sell_log_test", state).await
10546 else {
10547 return;
10548 };
10549 let order = test_market_buy_order(test_pool().instrument_id);
10550 cache
10551 .borrow_mut()
10552 .add_order(order.clone(), None, None, true)
10553 .unwrap();
10554 let mut receiver = start_with_events(&mut client);
10555 let plan = client
10556 .prepare_swap(&submit_order_cmd(&order), &order)
10557 .unwrap();
10558
10559 let error = execute_swap(
10560 plan,
10561 client.transaction_executor().unwrap(),
10562 client.emitter.clone(),
10563 client.transaction_limits.max_quote_age_blocks,
10564 client.transaction_limits.deadline_seconds,
10565 )
10566 .await
10567 .unwrap_err();
10568
10569 let events = collect_order_events(&mut receiver);
10570 assert_swap_quarantined_without_terminal_event(&events);
10571 assert!(
10572 error
10573 .to_string()
10574 .contains("does not match the persisted amount")
10575 || error.to_string().contains("is not a BUY output"),
10576 "was: {error}"
10577 );
10578 assert!(client.in_flight.lock().is_some());
10579
10580 drop_execution_schema(&admin_pool, &schema).await;
10581 }
10582
10583 #[tokio::test]
10584 async fn submit_order_applies_buy_slippage_to_base_output() {
10585 let amount_in = expected_buy_amount_in();
10586 let min_amount_out = expected_buy_min_amount_out(200);
10587 let (expected_hash, expected_raw) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10588 let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in);
10589 let Some((admin_pool, schema, mut client, state, cache)) =
10590 swap_client_with_buy_database("execution_submit_buy_slippage_test", state).await
10591 else {
10592 return;
10593 };
10594 let order = test_market_buy_order(test_pool().instrument_id);
10595 cache
10596 .borrow_mut()
10597 .add_order(order.clone(), None, None, true)
10598 .unwrap();
10599 let mut cmd = submit_order_cmd(&order);
10600 cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
10601 let mut receiver = start_with_events(&mut client);
10602
10603 client.submit_order(cmd).unwrap();
10604 await_pending_tasks(&client).await;
10605
10606 let events = collect_order_events(&mut receiver);
10607 assert_swap_submitted_and_filled(&events);
10608 let broadcasts: Vec<_> = state
10609 .recorded_requests()
10610 .into_iter()
10611 .filter(|request| request["method"] == "eth_sendRawTransaction")
10612 .collect();
10613 assert_eq!(broadcasts.len(), 1);
10614 assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
10615
10616 drop_execution_schema(&admin_pool, &schema).await;
10617 }
10618
10619 #[tokio::test]
10620 async fn submit_order_denies_buy_on_insufficient_quote_balance() {
10621 let amount_in = expected_buy_amount_in();
10622 let min_amount_out = expected_buy_min_amount_out(50);
10623 let (expected_hash, _) = expected_buy_swap_tx(min_amount_out, amount_in).await;
10624 let state = finalized_buy_swap_rpc_state(expected_hash, min_amount_out, amount_in)
10625 .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10626 let Some((admin_pool, schema, mut client, _, cache)) =
10627 swap_client_with_buy_database("execution_submit_buy_balance_test", state).await
10628 else {
10629 return;
10630 };
10631 let order = test_market_buy_order(test_pool().instrument_id);
10632 cache
10633 .borrow_mut()
10634 .add_order(order.clone(), None, None, true)
10635 .unwrap();
10636 let mut receiver = start_with_events(&mut client);
10637
10638 client.submit_order(submit_order_cmd(&order)).unwrap();
10639 await_pending_tasks(&client).await;
10640
10641 let events = collect_order_events(&mut receiver);
10642 assert_eq!(events.len(), 1);
10643 let OrderEventAny::Denied(denied) = &events[0] else {
10644 panic!("expected OrderDenied, was {:?}", events[0]);
10645 };
10646 assert!(
10647 denied.reason.contains("is below the swap amount"),
10648 "was: {}",
10649 denied.reason
10650 );
10651
10652 drop_execution_schema(&admin_pool, &schema).await;
10653 }
10654
10655 #[tokio::test]
10656 async fn finalized_swap_without_log_stays_quarantined() {
10657 let state = swap_rpc_state()
10658 .await
10659 .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS);
10660 let Some((admin_pool, schema, mut client, _, _)) =
10661 swap_client_with_database("execution_submit_missing_log_test", state).await
10662 else {
10663 return;
10664 };
10665 let order = test_market_sell_order(test_pool().instrument_id);
10666 let mut receiver = start_with_events(&mut client);
10667
10668 client.submit_order(submit_order_cmd(&order)).unwrap();
10669 await_pending_tasks(&client).await;
10670
10671 let events = collect_order_events(&mut receiver);
10672 let (status, terminal_emitted, active): (String, bool, bool) =
10673 sqlx::query_as(sqlx::AssertSqlSafe(format!(
10674 "SELECT status, terminal_emitted, active FROM {schema}.execution_intent"
10675 )))
10676 .fetch_one(&admin_pool)
10677 .await
10678 .unwrap();
10679
10680 assert_swap_quarantined_without_terminal_event(&events);
10681 assert_eq!(status, "broadcast");
10682 assert!(!terminal_emitted);
10683 assert!(active);
10684 assert!(client.in_flight.lock().is_some());
10685
10686 drop_execution_schema(&admin_pool, &schema).await;
10687 }
10688
10689 #[tokio::test]
10690 async fn finalized_swap_refresh_failure_stays_owned_for_reconciliation() {
10691 let min_amount_out = expected_min_amount_out(50);
10692 let (expected_hash, _) = expected_swap_tx(min_amount_out).await;
10693 let state = finalized_swap_rpc_state(expected_hash, min_amount_out).with_response_sequence(
10694 "eth_getBalance",
10695 &[
10696 GET_BALANCE,
10697 GET_BALANCE,
10698 GET_BALANCE,
10699 RPC_METHOD_NOT_FOUND,
10700 RPC_METHOD_NOT_FOUND,
10701 RPC_METHOD_NOT_FOUND,
10702 ],
10703 );
10704 let Some((admin_pool, schema, mut client, _state, _)) =
10705 swap_client_with_database("execution_submit_refresh_fail_test", state).await
10706 else {
10707 return;
10708 };
10709 let order = test_market_sell_order(test_pool().instrument_id);
10710 let mut receiver = start_with_events(&mut client);
10711 let plan = client
10712 .prepare_swap(&submit_order_cmd(&order), &order)
10713 .unwrap();
10714
10715 let error = execute_swap(
10716 plan,
10717 client.transaction_executor().unwrap(),
10718 client.emitter.clone(),
10719 client.transaction_limits.max_quote_age_blocks,
10720 client.transaction_limits.deadline_seconds,
10721 )
10722 .await
10723 .unwrap_err();
10724 let events = collect_order_events(&mut receiver);
10725 let (status, fill_emitted, active): (String, bool, bool) =
10726 sqlx::query_as(sqlx::AssertSqlSafe(format!(
10727 "SELECT status, fill_emitted, active FROM {schema}.execution_intent"
10728 )))
10729 .fetch_one(&admin_pool)
10730 .await
10731 .unwrap();
10732
10733 assert!(
10734 error
10735 .to_string()
10736 .contains("finalized native balance verification is locally invalid"),
10737 "was: {error}"
10738 );
10739 assert_eq!(events.len(), 1, "was: {events:?}");
10740 assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
10741 assert_eq!(status, "broadcast");
10742 assert!(!fill_emitted);
10743 assert!(active);
10744 assert!(client.in_flight.lock().is_some());
10745
10746 drop_execution_schema(&admin_pool, &schema).await;
10747 }
10748
10749 #[tokio::test]
10750 async fn submit_order_denies_router_factory_mismatch_before_signing() {
10751 let state = swap_rpc_state()
10752 .await
10753 .with_call_response(FACTORY_SELECTOR, CALL_ZERO);
10754 let Some((admin_pool, schema, mut client, state, _)) =
10755 swap_client_with_database("execution_submit_factory_test", state).await
10756 else {
10757 return;
10758 };
10759 let order = test_market_sell_order(test_pool().instrument_id);
10760 let mut receiver = start_with_events(&mut client);
10761
10762 client.submit_order(submit_order_cmd(&order)).unwrap();
10763 await_pending_tasks(&client).await;
10764
10765 let events = collect_order_events(&mut receiver);
10766 assert_eq!(events.len(), 1);
10767 let OrderEventAny::Denied(denied) = &events[0] else {
10768 panic!("expected OrderDenied, was {:?}", events[0]);
10769 };
10770 assert!(
10771 denied
10772 .reason
10773 .contains("swap deployment manifest verification disagreed"),
10774 "was: {}",
10775 denied.reason
10776 );
10777 let requests = state.recorded_requests();
10778 assert!(
10779 requests
10780 .iter()
10781 .all(|request| request["method"] != "eth_getTransactionCount")
10782 );
10783 assert!(
10784 requests
10785 .iter()
10786 .all(|request| request["method"] != "eth_sendRawTransaction")
10787 );
10788
10789 drop_execution_schema(&admin_pool, &schema).await;
10790 }
10791
10792 #[tokio::test]
10793 async fn submit_order_denies_router_weth_mismatch_before_signing() {
10794 let state = swap_rpc_state()
10795 .await
10796 .with_call_response(WETH9_SELECTOR, CALL_ZERO);
10797 let Some((admin_pool, schema, mut client, state, _)) =
10798 swap_client_with_database("execution_submit_weth_test", state).await
10799 else {
10800 return;
10801 };
10802 let order = test_market_sell_order(test_pool().instrument_id);
10803 let mut receiver = start_with_events(&mut client);
10804
10805 client.submit_order(submit_order_cmd(&order)).unwrap();
10806 await_pending_tasks(&client).await;
10807
10808 let events = collect_order_events(&mut receiver);
10809 assert_eq!(events.len(), 1);
10810 let OrderEventAny::Denied(denied) = &events[0] else {
10811 panic!("expected OrderDenied, was {:?}", events[0]);
10812 };
10813 assert!(
10814 denied
10815 .reason
10816 .contains("swap deployment manifest verification disagreed"),
10817 "was: {}",
10818 denied.reason
10819 );
10820 let requests = state.recorded_requests();
10821 assert!(
10822 requests
10823 .iter()
10824 .all(|request| request["method"] != "eth_getTransactionCount")
10825 );
10826 assert!(
10827 requests
10828 .iter()
10829 .all(|request| request["method"] != "eth_sendRawTransaction")
10830 );
10831
10832 drop_execution_schema(&admin_pool, &schema).await;
10833 }
10834
10835 #[tokio::test]
10836 async fn submit_order_denies_factory_pool_mismatch_before_signing() {
10837 let state = swap_rpc_state()
10838 .await
10839 .with_call_response(GET_POOL_SELECTOR, CALL_ZERO);
10840 let Some((admin_pool, schema, mut client, state, _)) =
10841 swap_client_with_database("execution_submit_pool_identity_test", state).await
10842 else {
10843 return;
10844 };
10845 let order = test_market_sell_order(test_pool().instrument_id);
10846 let mut receiver = start_with_events(&mut client);
10847
10848 client.submit_order(submit_order_cmd(&order)).unwrap();
10849 await_pending_tasks(&client).await;
10850
10851 let events = collect_order_events(&mut receiver);
10852 assert_eq!(events.len(), 1);
10853 let OrderEventAny::Denied(denied) = &events[0] else {
10854 panic!("expected OrderDenied, was {:?}", events[0]);
10855 };
10856 assert!(
10857 denied
10858 .reason
10859 .contains("swap deployment manifest verification disagreed"),
10860 "was: {}",
10861 denied.reason
10862 );
10863 let requests = state.recorded_requests();
10864 assert!(
10865 requests
10866 .iter()
10867 .all(|request| request["method"] != "eth_getTransactionCount")
10868 );
10869 assert!(
10870 requests
10871 .iter()
10872 .all(|request| request["method"] != "eth_sendRawTransaction")
10873 );
10874
10875 drop_execution_schema(&admin_pool, &schema).await;
10876 }
10877
10878 #[tokio::test]
10879 async fn submit_order_denies_cached_token_decimal_mismatch_before_signing() {
10880 let state = swap_rpc_state().await.with_contract_call_response(
10881 WETH,
10882 DECIMALS_SELECTOR,
10883 CALL_DECIMALS_6,
10884 );
10885 let Some((admin_pool, schema, mut client, state, _)) =
10886 swap_client_with_database("execution_submit_decimals_test", state).await
10887 else {
10888 return;
10889 };
10890 let order = test_market_sell_order(test_pool().instrument_id);
10891 let mut receiver = start_with_events(&mut client);
10892
10893 client.submit_order(submit_order_cmd(&order)).unwrap();
10894 await_pending_tasks(&client).await;
10895
10896 let events = collect_order_events(&mut receiver);
10897 assert_eq!(events.len(), 1);
10898 let OrderEventAny::Denied(denied) = &events[0] else {
10899 panic!("expected OrderDenied, was {:?}", events[0]);
10900 };
10901 assert!(
10902 denied
10903 .reason
10904 .contains("swap deployment manifest verification disagreed"),
10905 "was: {}",
10906 denied.reason
10907 );
10908 let requests = state.recorded_requests();
10909 assert!(
10910 requests
10911 .iter()
10912 .all(|request| request["method"] != "eth_getTransactionCount")
10913 );
10914 assert!(
10915 requests
10916 .iter()
10917 .all(|request| request["method"] != "eth_sendRawTransaction")
10918 );
10919
10920 drop_execution_schema(&admin_pool, &schema).await;
10921 }
10922
10923 #[tokio::test]
10924 async fn submit_order_denies_on_insufficient_router_allowance() {
10925 let state = swap_rpc_state()
10926 .await
10927 .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
10928 let Some((admin_pool, schema, mut client, state, _)) =
10929 swap_client_with_database("execution_submit_allowance_test", state).await
10930 else {
10931 return;
10932 };
10933 let order = test_market_sell_order(test_pool().instrument_id);
10934 let mut receiver = start_with_events(&mut client);
10935
10936 client.submit_order(submit_order_cmd(&order)).unwrap();
10937 await_pending_tasks(&client).await;
10938
10939 let events = collect_order_events(&mut receiver);
10940 assert_eq!(events.len(), 1);
10941 let OrderEventAny::Denied(denied) = &events[0] else {
10942 panic!("expected OrderDenied, was {:?}", events[0]);
10943 };
10944 assert!(
10945 denied.reason.contains("below the swap amount"),
10946 "was: {}",
10947 denied.reason
10948 );
10949 let requests = state.recorded_requests();
10950 assert!(
10951 requests
10952 .iter()
10953 .all(|request| request["method"] != "eth_sendRawTransaction"),
10954 "no broadcast may follow a pre-trade denial"
10955 );
10956 let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
10957 "SELECT COUNT(*) FROM {schema}.execution_intent"
10958 )))
10959 .fetch_one(&admin_pool)
10960 .await
10961 .unwrap();
10962 assert_eq!(row_count, 0);
10963 assert!(client.in_flight.lock().is_none());
10964
10965 drop_execution_schema(&admin_pool, &schema).await;
10966 }
10967
10968 #[tokio::test]
10969 async fn submit_order_denies_on_insufficient_input_balance() {
10970 let state = swap_rpc_state()
10971 .await
10972 .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO);
10973 let Some((admin_pool, schema, mut client, _, _)) =
10974 swap_client_with_database("execution_submit_balance_test", state).await
10975 else {
10976 return;
10977 };
10978 let order = test_market_sell_order(test_pool().instrument_id);
10979 let mut receiver = start_with_events(&mut client);
10980
10981 client.submit_order(submit_order_cmd(&order)).unwrap();
10982 await_pending_tasks(&client).await;
10983
10984 let events = collect_order_events(&mut receiver);
10985 assert_eq!(events.len(), 1);
10986 let OrderEventAny::Denied(denied) = &events[0] else {
10987 panic!("expected OrderDenied, was {:?}", events[0]);
10988 };
10989 assert!(
10990 denied.reason.contains("is below the swap amount"),
10991 "was: {}",
10992 denied.reason
10993 );
10994
10995 drop_execution_schema(&admin_pool, &schema).await;
10996 }
10997
10998 #[tokio::test]
10999 async fn submit_order_denies_on_insufficient_native_balance() {
11000 let state = swap_rpc_state()
11001 .await
11002 .with_response("eth_getBalance", GET_BALANCE_INSUFFICIENT);
11003 let Some((admin_pool, schema, mut client, state, _)) =
11004 swap_client_with_database("execution_submit_native_balance_test", state).await
11005 else {
11006 return;
11007 };
11008 let order = test_market_sell_order(test_pool().instrument_id);
11009 let mut receiver = start_with_events(&mut client);
11010
11011 client.submit_order(submit_order_cmd(&order)).unwrap();
11012 await_pending_tasks(&client).await;
11013
11014 let events = collect_order_events(&mut receiver);
11015 assert_eq!(events.len(), 1);
11016 let OrderEventAny::Denied(denied) = &events[0] else {
11017 panic!("expected OrderDenied, was {:?}", events[0]);
11018 };
11019 assert!(
11020 denied.reason.contains("below maximum transaction cost"),
11021 "was: {}",
11022 denied.reason
11023 );
11024 assert!(
11025 state
11026 .recorded_requests()
11027 .iter()
11028 .all(|request| request["method"] != "eth_sendRawTransaction"),
11029 "no broadcast may follow an insufficient native balance"
11030 );
11031 let (row_count, status): (i64, Option<String>) = sqlx::query_as(sqlx::AssertSqlSafe(
11032 format!("SELECT COUNT(*), MAX(status) FROM {schema}.execution_intent"),
11033 ))
11034 .fetch_one(&admin_pool)
11035 .await
11036 .unwrap();
11037 assert_eq!(row_count, 1);
11038 assert_eq!(status.as_deref(), Some("recoverable"));
11039 assert!(client.in_flight.lock().is_none());
11040
11041 drop_execution_schema(&admin_pool, &schema).await;
11042 }
11043
11044 #[tokio::test]
11045 async fn submit_order_denies_on_stale_quote() {
11046 let Some((admin_pool, schema, mut client, state, cache)) =
11047 swap_client_with_database("execution_submit_stale_quote_test", swap_rpc_state().await)
11048 .await
11049 else {
11050 return;
11051 };
11052 let pool = test_pool();
11053 cache
11054 .borrow_mut()
11055 .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK - 101))
11056 .unwrap();
11057 let order = test_market_sell_order(pool.instrument_id);
11058 let mut receiver = start_with_events(&mut client);
11059
11060 client.submit_order(submit_order_cmd(&order)).unwrap();
11061 await_pending_tasks(&client).await;
11062
11063 let events = collect_order_events(&mut receiver);
11064 assert_eq!(events.len(), 1);
11065 let OrderEventAny::Denied(denied) = &events[0] else {
11066 panic!("expected OrderDenied, was {:?}", events[0]);
11067 };
11068 assert!(
11069 denied.reason.contains("Stale quote"),
11070 "was: {}",
11071 denied.reason
11072 );
11073 let requests = state.recorded_requests();
11074 assert!(
11075 requests
11076 .iter()
11077 .all(|request| request["method"] != "eth_sendRawTransaction"),
11078 "no broadcast may follow a pre-trade denial"
11079 );
11080
11081 drop_execution_schema(&admin_pool, &schema).await;
11082 }
11083
11084 #[tokio::test]
11085 async fn submit_order_denies_quote_ahead_of_chain_head() {
11086 let Some((admin_pool, schema, mut client, state, cache)) =
11087 swap_client_with_database("execution_submit_ahead_quote_test", swap_rpc_state().await)
11088 .await
11089 else {
11090 return;
11091 };
11092 let pool = test_pool();
11093 cache
11094 .borrow_mut()
11095 .add_pool_profiler(test_profiler(&pool, FIXTURE_BLOCK + 1))
11096 .unwrap();
11097 let order = test_market_sell_order(pool.instrument_id);
11098 let mut receiver = start_with_events(&mut client);
11099
11100 client.submit_order(submit_order_cmd(&order)).unwrap();
11101 await_pending_tasks(&client).await;
11102
11103 let events = collect_order_events(&mut receiver);
11104 assert_eq!(events.len(), 1);
11105 let OrderEventAny::Denied(denied) = &events[0] else {
11106 panic!("expected OrderDenied, was {:?}", events[0]);
11107 };
11108 assert!(
11109 denied.reason.contains("is ahead of the latest block"),
11110 "was: {}",
11111 denied.reason
11112 );
11113 let requests = state.recorded_requests();
11114 assert!(
11115 requests
11116 .iter()
11117 .all(|request| request["method"] != "eth_sendRawTransaction"),
11118 "no broadcast may follow a pre-trade denial"
11119 );
11120
11121 drop_execution_schema(&admin_pool, &schema).await;
11122 }
11123
11124 #[tokio::test]
11125 async fn submit_order_denies_on_deadline_overflow() {
11126 let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
11127 "execution_submit_deadline_overflow_test",
11128 swap_rpc_state().await,
11129 )
11130 .await
11131 else {
11132 return;
11133 };
11134 client.transaction_limits.deadline_seconds = u64::MAX;
11135 let order = test_market_sell_order(test_pool().instrument_id);
11136 let mut receiver = start_with_events(&mut client);
11137
11138 client.submit_order(submit_order_cmd(&order)).unwrap();
11139 await_pending_tasks(&client).await;
11140
11141 let events = collect_order_events(&mut receiver);
11142 assert_eq!(events.len(), 1);
11143 let OrderEventAny::Denied(denied) = &events[0] else {
11144 panic!("expected OrderDenied, was {:?}", events[0]);
11145 };
11146 assert!(
11147 denied.reason.contains("deadline overflow"),
11148 "was: {}",
11149 denied.reason
11150 );
11151 let requests = state.recorded_requests();
11152 assert!(
11153 requests
11154 .iter()
11155 .all(|request| request["method"] != "eth_sendRawTransaction"),
11156 "no broadcast may follow a pre-trade denial"
11157 );
11158 assert!(client.in_flight.lock().is_none());
11159
11160 drop_execution_schema(&admin_pool, &schema).await;
11161 }
11162
11163 #[tokio::test]
11164 async fn submit_order_reconciles_node_rejection_to_finalized_receipt() {
11165 let state = swap_rpc_state()
11166 .await
11167 .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED);
11168 let Some((admin_pool, schema, mut client, _state, _)) =
11169 swap_client_with_database("execution_submit_node_rejected_test", state).await
11170 else {
11171 return;
11172 };
11173 let order = test_market_sell_order(test_pool().instrument_id);
11174 let mut receiver = start_with_events(&mut client);
11175
11176 client.submit_order(submit_order_cmd(&order)).unwrap();
11177 await_pending_tasks(&client).await;
11178
11179 let events = collect_order_events(&mut receiver);
11180 assert_swap_submitted_and_filled(&events);
11181 let (purpose, status, client_order_id): (String, String, Option<String>) =
11182 sqlx::query_as(sqlx::AssertSqlSafe(format!(
11183 "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11184 )))
11185 .fetch_one(&admin_pool)
11186 .await
11187 .unwrap();
11188 assert_eq!(purpose, "swap");
11189 assert_eq!(status, "finalized");
11190 assert_eq!(
11191 client_order_id.as_deref(),
11192 Some(order.client_order_id().as_str())
11193 );
11194 assert!(client.in_flight.lock().is_none());
11195
11196 drop_execution_schema(&admin_pool, &schema).await;
11197 }
11198
11199 #[tokio::test]
11200 async fn submit_order_acknowledges_uncertain_nonce_too_low() {
11201 let state = swap_rpc_state()
11202 .await
11203 .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_NONCE_TOO_LOW);
11204 let Some((admin_pool, schema, mut client, _state, _)) =
11205 swap_client_with_database("execution_submit_nonce_too_low_test", state).await
11206 else {
11207 return;
11208 };
11209 let order = test_market_sell_order(test_pool().instrument_id);
11210 let mut receiver = start_with_events(&mut client);
11211
11212 client.submit_order(submit_order_cmd(&order)).unwrap();
11213 await_pending_tasks(&client).await;
11214
11215 let events = collect_order_events(&mut receiver);
11216 assert_swap_submitted_and_filled(&events);
11217 let (purpose, status, client_order_id): (String, String, Option<String>) =
11218 sqlx::query_as(sqlx::AssertSqlSafe(format!(
11219 "SELECT purpose, status, client_order_id FROM {schema}.execution_intent"
11220 )))
11221 .fetch_one(&admin_pool)
11222 .await
11223 .unwrap();
11224 assert_eq!(purpose, "swap");
11225 assert_eq!(status, "finalized");
11226 assert_eq!(
11227 client_order_id.as_deref(),
11228 Some(order.client_order_id().as_str())
11229 );
11230 assert!(client.in_flight.lock().is_none());
11231
11232 drop_execution_schema(&admin_pool, &schema).await;
11233 }
11234
11235 #[tokio::test]
11236 async fn submit_order_rejected_on_reverted_receipt() {
11237 let expected_min_out = expected_min_amount_out(50);
11238 let (expected_hash, _) = expected_swap_tx(expected_min_out).await;
11239 let block = finalized_swap_block(expected_hash, expected_min_out);
11240 let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
11241 let state = with_finalized_identity(
11242 swap_rpc_state()
11243 .await
11244 .with_response("eth_getTransactionReceipt", &receipt),
11245 &block,
11246 &receipt,
11247 );
11248 let Some((admin_pool, schema, mut client, _state, _)) =
11249 swap_client_with_database("execution_submit_reverted_test", state).await
11250 else {
11251 return;
11252 };
11253 let order = test_market_sell_order(test_pool().instrument_id);
11254 let mut receiver = start_with_events(&mut client);
11255 client.submit_order(submit_order_cmd(&order)).unwrap();
11256 await_pending_tasks(&client).await;
11257
11258 let events = collect_order_events(&mut receiver);
11259 assert_eq!(events.len(), 2);
11260 assert!(
11261 matches!(&events[0], OrderEventAny::Submitted(_)),
11262 "was: {:?}",
11263 events[0]
11264 );
11265 let OrderEventAny::Rejected(rejected) = &events[1] else {
11266 panic!("expected OrderRejected, was {:?}", events[1]);
11267 };
11268 assert!(
11269 rejected.reason.contains("reverted on-chain"),
11270 "was: {}",
11271 rejected.reason
11272 );
11273
11274 let record = client
11275 .cache
11276 .get_execution_transaction(42161, &expected_hash.to_string())
11277 .await
11278 .unwrap()
11279 .unwrap();
11280 assert_eq!(record.purpose, "swap");
11281 assert_eq!(record.status, "reverted");
11282 assert_eq!(
11283 record.client_order_id.as_deref(),
11284 Some(order.client_order_id().as_str())
11285 );
11286 assert!(client.in_flight.lock().is_none());
11287
11288 drop_execution_schema(&admin_pool, &schema).await;
11289 }
11290
11291 #[tokio::test]
11292 async fn submit_order_acknowledges_ambiguous_broadcast() {
11293 let state = swap_rpc_state()
11296 .await
11297 .with_response("eth_sendRawTransaction", "not json");
11298 let Some((admin_pool, schema, mut client, _state, _)) =
11299 swap_client_with_database("execution_submit_ambiguous_test", state).await
11300 else {
11301 return;
11302 };
11303 let order = test_market_sell_order(test_pool().instrument_id);
11304 let mut receiver = start_with_events(&mut client);
11305
11306 client.submit_order(submit_order_cmd(&order)).unwrap();
11307 await_pending_tasks(&client).await;
11308
11309 let events = collect_order_events(&mut receiver);
11310 assert_swap_submitted_and_filled(&events);
11311
11312 let record = client
11313 .cache
11314 .get_execution_transaction(
11315 42161,
11316 &expected_swap_tx(expected_min_amount_out(50))
11317 .await
11318 .0
11319 .to_string(),
11320 )
11321 .await
11322 .unwrap()
11323 .unwrap();
11324 assert_eq!(record.purpose, "swap");
11325 assert_eq!(record.status, "finalized");
11326 assert!(client.in_flight.lock().is_none());
11327
11328 drop_execution_schema(&admin_pool, &schema).await;
11329 }
11330
11331 #[tokio::test]
11332 async fn submit_order_acknowledges_broadcast_hash_mismatch() {
11333 let state = swap_rpc_state_for_mismatch().await;
11336 let Some((admin_pool, schema, mut client, _state, _)) =
11337 swap_client_with_database("execution_submit_hash_mismatch_test", state).await
11338 else {
11339 return;
11340 };
11341 let order = test_market_sell_order(test_pool().instrument_id);
11342 let mut receiver = start_with_events(&mut client);
11343
11344 client.submit_order(submit_order_cmd(&order)).unwrap();
11345 await_pending_tasks(&client).await;
11346
11347 let events = collect_order_events(&mut receiver);
11348 assert_swap_submitted_and_filled(&events);
11349
11350 let record = client
11351 .cache
11352 .get_execution_transaction(
11353 42161,
11354 &expected_swap_tx(expected_min_amount_out(50))
11355 .await
11356 .0
11357 .to_string(),
11358 )
11359 .await
11360 .unwrap()
11361 .unwrap();
11362 assert_eq!(record.purpose, "swap");
11363 assert_eq!(record.status, "finalized");
11364 assert!(client.in_flight.lock().is_none());
11365
11366 drop_execution_schema(&admin_pool, &schema).await;
11367 }
11368
11369 #[tokio::test]
11370 async fn submit_order_persists_authenticated_envelope_before_broadcast() {
11371 let state = swap_rpc_state().await;
11372 let Some((admin_pool, schema, mut client, state, _)) =
11373 swap_client_with_database("protected_submit_test", state).await
11374 else {
11375 return;
11376 };
11377 let order = test_market_sell_order(test_pool().instrument_id);
11378 let mut receiver = start_with_events(&mut client);
11379
11380 client.submit_order(submit_order_cmd(&order)).unwrap();
11381 await_pending_tasks(&client).await;
11382
11383 let events = collect_order_events(&mut receiver);
11384 assert_swap_submitted_and_filled(&events);
11385 let representations: Vec<(bool, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
11386 "SELECT raw_transaction IS NULL, sealed_transaction IS NOT NULL \
11387 FROM {schema}.execution_transaction_hash WHERE payload_expected"
11388 )))
11389 .fetch_all(&admin_pool)
11390 .await
11391 .unwrap();
11392 let broadcasts = state
11393 .recorded_requests()
11394 .into_iter()
11395 .filter(|request| request["method"] == "eth_sendRawTransaction")
11396 .count();
11397 assert_eq!(representations, vec![(true, true)]);
11398 assert_eq!(broadcasts, 1);
11399
11400 drop_execution_schema(&admin_pool, &schema).await;
11401 }
11402
11403 #[tokio::test]
11404 async fn protected_persistence_failure_prevents_broadcast_and_acknowledgment() {
11405 let state = swap_rpc_state().await;
11406 let Some((admin_pool, schema, mut client, state, _)) =
11407 swap_client_with_database("protected_persist_failure_test", state).await
11408 else {
11409 return;
11410 };
11411
11412 for statement in [
11413 format!(
11414 "CREATE FUNCTION {schema}.reject_protected_payload() RETURNS trigger \
11415 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test payload rejection''; END'"
11416 ),
11417 format!(
11418 "CREATE TRIGGER reject_protected_payload BEFORE INSERT ON \
11419 {schema}.execution_transaction_hash FOR EACH ROW \
11420 EXECUTE FUNCTION {schema}.reject_protected_payload()"
11421 ),
11422 ] {
11423 sqlx::query(sqlx::AssertSqlSafe(statement))
11424 .execute(&admin_pool)
11425 .await
11426 .unwrap();
11427 }
11428 let order = test_market_sell_order(test_pool().instrument_id);
11429 let mut receiver = start_with_events(&mut client);
11430
11431 client.submit_order(submit_order_cmd(&order)).unwrap();
11432 await_pending_tasks(&client).await;
11433
11434 let events = collect_order_events(&mut receiver);
11435 assert!(
11436 events
11437 .iter()
11438 .all(|event| !matches!(event, OrderEventAny::Submitted(_)))
11439 );
11440 let broadcasts = state
11441 .recorded_requests()
11442 .into_iter()
11443 .filter(|request| request["method"] == "eth_sendRawTransaction")
11444 .count();
11445 let payload_rows: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11446 "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11447 )))
11448 .fetch_one(&admin_pool)
11449 .await
11450 .unwrap();
11451 assert_eq!(broadcasts, 0);
11452 assert_eq!(payload_rows, 0);
11453
11454 drop_execution_schema(&admin_pool, &schema).await;
11455 }
11456
11457 #[tokio::test]
11458 async fn submit_order_reservation_failure_denies_without_broadcast_and_releases_slot() {
11459 let Some((admin_pool, schema, mut client, state, cache)) =
11460 swap_client_with_database("execution_submit_persist_fail_test", swap_rpc_state().await)
11461 .await
11462 else {
11463 return;
11464 };
11465 sqlx::query(sqlx::AssertSqlSafe(format!(
11466 "DROP TABLE {schema}.execution_intent CASCADE"
11467 )))
11468 .execute(&admin_pool)
11469 .await
11470 .unwrap();
11471 let pool = test_pool();
11472 let first = test_market_sell_order(pool.instrument_id);
11473 let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11474 cache
11475 .borrow_mut()
11476 .add_order(second.clone(), None, None, false)
11477 .unwrap();
11478 let mut receiver = start_with_events(&mut client);
11479
11480 client.submit_order(submit_order_cmd(&first)).unwrap();
11481 await_pending_tasks(&client).await;
11482
11483 let events = collect_order_events(&mut receiver);
11484 assert_eq!(events.len(), 1);
11485 let OrderEventAny::Denied(denied) = &events[0] else {
11486 panic!("expected OrderDenied, was {:?}", events[0]);
11487 };
11488 assert!(
11489 denied
11490 .reason
11491 .contains("Execution intent reservation failed before commit"),
11492 "was: {}",
11493 denied.reason
11494 );
11495 client.submit_order(submit_order_cmd(&second)).unwrap();
11496 await_pending_tasks(&client).await;
11497 let retry_events = collect_order_events(&mut receiver);
11498 assert_eq!(retry_events.len(), 1);
11499 let OrderEventAny::Denied(retry_denied) = &retry_events[0] else {
11500 panic!("expected OrderDenied, was {:?}", retry_events[0]);
11501 };
11502 assert_eq!(
11503 retry_denied.reason,
11504 "Execution intent reservation failed before commit"
11505 );
11506 let broadcasts = state
11507 .recorded_requests()
11508 .into_iter()
11509 .filter(|request| request["method"] == "eth_sendRawTransaction")
11510 .count();
11511 assert_eq!(broadcasts, 0);
11512 assert!(client.in_flight.lock().is_none());
11513
11514 drop_execution_schema(&admin_pool, &schema).await;
11515 }
11516
11517 #[tokio::test]
11518 async fn submit_order_reservation_commit_failure_keeps_preparing_slot() {
11519 let Some((admin_pool, schema, mut client, state, cache)) = swap_client_with_database(
11520 "execution_submit_reservation_commit_fail_test",
11521 swap_rpc_state().await,
11522 )
11523 .await
11524 else {
11525 return;
11526 };
11527 install_reservation_commit_rejection(&admin_pool, &schema).await;
11528 let pool = test_pool();
11529 let first = test_market_sell_order(pool.instrument_id);
11530 let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
11531 cache
11532 .borrow_mut()
11533 .add_order(second.clone(), None, None, false)
11534 .unwrap();
11535 let mut receiver = start_with_events(&mut client);
11536
11537 client.submit_order(submit_order_cmd(&first)).unwrap();
11538 await_pending_tasks(&client).await;
11539 client.submit_order(submit_order_cmd(&second)).unwrap();
11540 await_pending_tasks(&client).await;
11541
11542 let events = collect_order_events(&mut receiver);
11543 let submitted = events
11544 .iter()
11545 .filter(|event| matches!(event, OrderEventAny::Submitted(_)))
11546 .count();
11547 let denied_commit = events
11548 .iter()
11549 .filter(|event| {
11550 matches!(event, OrderEventAny::Denied(denied) if denied.reason == "Execution intent reservation commit outcome is unknown; reconciliation is required")
11551 })
11552 .count();
11553 let denied_in_flight = events
11554 .iter()
11555 .filter(|event| {
11556 matches!(event, OrderEventAny::Denied(denied) if denied.reason.contains("at most one transaction can be in flight"))
11557 })
11558 .count();
11559 let requests = state.recorded_requests();
11560 let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11561 "SELECT COUNT(*) FROM {schema}.execution_intent"
11562 )))
11563 .fetch_one(&admin_pool)
11564 .await
11565 .unwrap();
11566 let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11567 "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
11568 )))
11569 .fetch_one(&admin_pool)
11570 .await
11571 .unwrap();
11572
11573 assert_eq!(submitted, 0, "was: {events:?}");
11574 assert_eq!(denied_commit, 1, "was: {events:?}");
11575 assert_eq!(denied_in_flight, 1, "was: {events:?}");
11576 assert!(matches!(
11577 *client.in_flight.lock(),
11578 Some(InFlightSlot::Preparing(TransactionPurpose::Swap))
11579 ));
11580 assert_eq!(intent_count, 0);
11581 assert_eq!(signed_count, 0);
11582 assert!(
11583 requests
11584 .iter()
11585 .all(|request| request["method"] != "eth_getTransactionCount")
11586 );
11587 assert!(
11588 requests
11589 .iter()
11590 .all(|request| request["method"] != "eth_sendRawTransaction")
11591 );
11592
11593 drop_execution_schema(&admin_pool, &schema).await;
11594 }
11595
11596 #[tokio::test]
11597 async fn protected_payload_migration_failure_keeps_plaintext_and_blocks_ready() {
11598 let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
11599 "protected_payload_migration_failure",
11600 execution_rpc_state(),
11601 )
11602 .await
11603 else {
11604 return;
11605 };
11606 let database = client.cache.database.as_ref().unwrap();
11607 let (intent, _) = persist_invalid_test_swap(database, None).await;
11608 let keys = payload_test_keys([0x31; 32], vec![], "migration-failure");
11609 let error = database
11610 .ensure_execution_payload_storage(&keys)
11611 .await
11612 .unwrap_err();
11613
11614 assert!(
11615 error
11616 .to_string()
11617 .contains("failed to authenticate execution payload")
11618 );
11619 let row = database
11620 .get_execution_transaction_hashes(intent.id)
11621 .await
11622 .unwrap()
11623 .pop()
11624 .unwrap();
11625 let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11626 "SELECT operation FROM {schema}.execution_payload_state"
11627 )))
11628 .fetch_one(&admin_pool)
11629 .await
11630 .unwrap();
11631 assert_eq!(operation, "migrate");
11632 assert!(row.raw_transaction.is_some());
11633 assert!(row.sealed_transaction.is_none());
11634
11635 drop_execution_schema(&admin_pool, &schema).await;
11636 }
11637
11638 #[tokio::test]
11639 async fn protected_payload_migration_restart_restore_rewrap_and_rollback() {
11640 let Some((admin_pool, pg_config)) =
11641 connect_test_postgres("protected payload lifecycle").await
11642 else {
11643 return;
11644 };
11645 let schema = format!("protected_payload_lifecycle_{}", std::process::id());
11646 setup_execution_schema(&admin_pool, &schema).await;
11647 let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11648 let options = options.options([("search_path", schema.clone())]);
11649 let database = connect_test_database(options.clone()).await.unwrap();
11650 database
11651 .ensure_execution_transaction_schema()
11652 .await
11653 .unwrap();
11654 let intent = reserve_test_wrap_intent(&database).await;
11655 database
11656 .assign_execution_intent_nonce(intent.id, 7)
11657 .await
11658 .unwrap();
11659 let transaction = build_eip1559_transaction(
11660 42161,
11661 7,
11662 78_000,
11663 130_000_000,
11664 10_000_000,
11665 WETH_ADDRESS,
11666 U256::from(1_u64),
11667 Bytes::from(hex::decode("d0e30db0").unwrap()),
11668 );
11669 let (transaction_hash, raw_transaction) = sign_eip1559_transaction(
11670 transaction,
11671 &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
11672 )
11673 .await
11674 .unwrap();
11675 database
11676 .add_execution_transaction_hash(
11677 intent.id,
11678 42161,
11679 &transaction_hash.to_string(),
11680 &raw_transaction,
11681 )
11682 .await
11683 .unwrap();
11684 let keys = payload_test_keys([0x41; 32], vec![], "restore-a");
11685
11686 database
11687 .ensure_execution_payload_storage(&keys)
11688 .await
11689 .unwrap();
11690 let operation: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11691 "SELECT operation FROM {schema}.execution_payload_state"
11692 )))
11693 .fetch_one(&admin_pool)
11694 .await
11695 .unwrap();
11696 assert_eq!(operation, "ready");
11697 drop(database);
11698
11699 let restarted = connect_test_database(options).await.unwrap();
11700 restarted
11701 .ensure_execution_payload_storage(&keys)
11702 .await
11703 .unwrap();
11704 let row = restarted
11705 .get_execution_transaction_hashes(intent.id)
11706 .await
11707 .unwrap()
11708 .pop()
11709 .unwrap();
11710 assert!(row.raw_transaction.is_none());
11711 let sealed_transaction = row.sealed_transaction.clone().unwrap();
11712 let stored_intent = restarted.get_execution_intent(intent.id).await.unwrap();
11713 let context = payload_context(&stored_intent, &row, keys.deployment_id()).unwrap();
11714 let alternate_envelope = keys.seal(&raw_transaction, &context).unwrap();
11715 assert_ne!(alternate_envelope, sealed_transaction);
11716 let repeated = restarted
11717 .add_execution_transaction_envelope(
11718 intent.id,
11719 42161,
11720 &transaction_hash.to_string(),
11721 &alternate_envelope,
11722 )
11723 .await
11724 .unwrap();
11725 let check = restarted
11726 .check_execution_payload_storage(Some(&keys), None, 1)
11727 .await
11728 .unwrap();
11729 assert_eq!(
11730 repeated.sealed_transaction,
11731 Some(sealed_transaction.clone())
11732 );
11733 assert_eq!(check.plaintext_rows, 0);
11734 assert_eq!(check.original_rows, 1);
11735 assert_eq!(check.replacement_rows, 0);
11736 assert_eq!(check.authenticated_rows, 1);
11737 assert!(!check.read_roles.is_empty());
11738
11739 sqlx::query(sqlx::AssertSqlSafe(format!(
11740 "UPDATE {schema}.execution_payload_key_state SET seals = 4294967295"
11741 )))
11742 .execute(&admin_pool)
11743 .await
11744 .unwrap();
11745 let exhausted = restarted
11746 .reserve_execution_payload_seal(&keys)
11747 .await
11748 .unwrap_err();
11749 assert!(exhausted.to_string().contains("seal limit"));
11750 let seals: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
11751 "SELECT seals FROM {schema}.execution_payload_key_state"
11752 )))
11753 .fetch_one(&admin_pool)
11754 .await
11755 .unwrap();
11756 assert_eq!(seals, 4_294_967_295);
11757 sqlx::query(sqlx::AssertSqlSafe(format!(
11758 "UPDATE {schema}.execution_payload_key_state SET seals = 1"
11759 )))
11760 .execute(&admin_pool)
11761 .await
11762 .unwrap();
11763
11764 let missing_key = restarted
11765 .check_execution_payload_storage(None, None, 1)
11766 .await
11767 .unwrap_err();
11768 assert!(
11769 missing_key
11770 .to_string()
11771 .contains("no payload key is configured")
11772 );
11773 let restored_elsewhere = payload_test_keys([0x41; 32], vec![], "restore-b");
11774 let wrong_context = restarted
11775 .ensure_execution_payload_storage(&restored_elsewhere)
11776 .await
11777 .unwrap_err();
11778 assert!(wrong_context.to_string().contains("deployment ID"));
11779
11780 sqlx::query(sqlx::AssertSqlSafe(format!(
11781 "UPDATE {schema}.execution_transaction_hash \
11782 SET sealed_transaction = set_byte(sealed_transaction, octet_length(sealed_transaction) - 1, \
11783 get_byte(sealed_transaction, octet_length(sealed_transaction) - 1) # 1)"
11784 )))
11785 .execute(&admin_pool)
11786 .await
11787 .unwrap();
11788 assert!(
11789 restarted
11790 .check_execution_payload_storage(Some(&keys), None, 1)
11791 .await
11792 .is_err()
11793 );
11794 sqlx::query(sqlx::AssertSqlSafe(format!(
11795 "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11796 )))
11797 .bind(&sealed_transaction)
11798 .execute(&admin_pool)
11799 .await
11800 .unwrap();
11801
11802 for statement in [
11803 format!(
11804 "ALTER TABLE {schema}.execution_transaction_hash \
11805 DROP CONSTRAINT execution_transaction_payload_protected_check"
11806 ),
11807 format!("UPDATE {schema}.execution_transaction_hash SET sealed_transaction = NULL"),
11808 ] {
11809 sqlx::query(sqlx::AssertSqlSafe(statement))
11810 .execute(&admin_pool)
11811 .await
11812 .unwrap();
11813 }
11814 assert!(
11815 restarted
11816 .check_execution_payload_storage(Some(&keys), None, 1)
11817 .await
11818 .is_err()
11819 );
11820 sqlx::query(sqlx::AssertSqlSafe(format!(
11821 "UPDATE {schema}.execution_transaction_hash SET sealed_transaction = $1"
11822 )))
11823 .bind(&sealed_transaction)
11824 .execute(&admin_pool)
11825 .await
11826 .unwrap();
11827 sqlx::query(sqlx::AssertSqlSafe(format!(
11828 "ALTER TABLE {schema}.execution_transaction_hash \
11829 ADD CONSTRAINT execution_transaction_payload_protected_check CHECK ( \
11830 (payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NOT NULL) \
11831 OR (NOT payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NULL) \
11832 )"
11833 )))
11834 .execute(&admin_pool)
11835 .await
11836 .unwrap();
11837
11838 let rotated = payload_test_keys([0x52; 32], vec![[0x41; 32]], "restore-a");
11839 restarted
11840 .rewrap_execution_payload_storage(&rotated, 1)
11841 .await
11842 .unwrap();
11843 let rotated_check = restarted
11844 .check_execution_payload_storage(Some(&rotated), None, 1)
11845 .await
11846 .unwrap();
11847 assert_eq!(rotated_check.authenticated_rows, 1);
11848 assert_eq!(rotated_check.key_ids.len(), 1);
11849
11850 restarted
11851 .rollback_execution_payload_storage(&rotated, 1)
11852 .await
11853 .unwrap();
11854 let rolled_back = restarted
11855 .get_execution_transaction_hashes(intent.id)
11856 .await
11857 .unwrap()
11858 .pop()
11859 .unwrap();
11860 assert_eq!(
11861 rolled_back.raw_transaction.as_deref(),
11862 Some(raw_transaction.as_slice())
11863 );
11864 assert!(rolled_back.sealed_transaction.is_none());
11865 let legacy_check = restarted
11866 .check_execution_payload_storage(None, None, 1)
11867 .await
11868 .unwrap();
11869 assert!(!legacy_check.protected);
11870 assert_eq!(legacy_check.plaintext_rows, 1);
11871 assert_eq!(legacy_check.authenticated_rows, 1);
11872
11873 drop_execution_schema(&admin_pool, &schema).await;
11874 }
11875
11876 #[tokio::test]
11877 async fn protected_payload_storage_authenticates_multiple_execution_identities() {
11878 let Some((admin_pool, pg_config)) =
11879 connect_test_postgres("protected payload multiple identities").await
11880 else {
11881 return;
11882 };
11883 let schema = format!("protected_payload_identities_{}", std::process::id());
11884 setup_execution_schema(&admin_pool, &schema).await;
11885 let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11886 let database = connect_test_database(options.options([("search_path", schema.clone())]))
11887 .await
11888 .unwrap();
11889 database
11890 .ensure_execution_transaction_schema()
11891 .await
11892 .unwrap();
11893
11894 let private_keys = [
11895 TEST_PRIVATE_KEY,
11896 "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
11897 ];
11898
11899 for (index, private_key) in private_keys.into_iter().enumerate() {
11900 let signer = PrivateKeySigner::from_str(private_key).unwrap();
11901 let nonce = 7 + u64::try_from(index).unwrap();
11902 let intent =
11903 reserve_test_wrap_intent_for_wallet(&database, &signer.address().to_string()).await;
11904 database
11905 .assign_execution_intent_nonce(intent.id, nonce)
11906 .await
11907 .unwrap();
11908 let transaction = build_eip1559_transaction(
11909 42161,
11910 nonce,
11911 78_000,
11912 130_000_000,
11913 10_000_000,
11914 WETH_ADDRESS,
11915 U256::from(1_u64),
11916 Bytes::from(hex::decode("d0e30db0").unwrap()),
11917 );
11918 let (transaction_hash, raw_transaction) =
11919 sign_eip1559_transaction(transaction, &signer)
11920 .await
11921 .unwrap();
11922 database
11923 .add_execution_transaction_hash(
11924 intent.id,
11925 42161,
11926 &transaction_hash.to_string(),
11927 &raw_transaction,
11928 )
11929 .await
11930 .unwrap();
11931 }
11932
11933 let keys = payload_test_keys([0x61; 32], vec![], "multiple-identities");
11934 database
11935 .ensure_execution_payload_storage(&keys)
11936 .await
11937 .unwrap();
11938 let lease = database
11939 .require_execution_payload_storage(
11940 &keys,
11941 PayloadPolicy {
11942 chain_id: 42161,
11943 signer: Address::from_str(WALLET).unwrap(),
11944 gas_limit: 1_000_000,
11945 max_fee_per_gas: 1_000_000_000,
11946 },
11947 1,
11948 )
11949 .await
11950 .unwrap();
11951 drop(lease);
11952 let check = database
11953 .check_execution_payload_storage(Some(&keys), None, 1)
11954 .await
11955 .unwrap();
11956
11957 assert!(check.protected);
11958 assert_eq!(check.plaintext_rows, 0);
11959 assert_eq!(check.original_rows, 2);
11960 assert_eq!(check.replacement_rows, 0);
11961 assert_eq!(check.authenticated_rows, 2);
11962 assert_eq!(check.key_ids.len(), 1);
11963
11964 drop_execution_schema(&admin_pool, &schema).await;
11965 }
11966
11967 #[rstest]
11968 #[case::finalized(TransactionStatus::Finalized)]
11969 #[case::reverted(TransactionStatus::Reverted)]
11970 #[tokio::test]
11971 async fn protected_payload_storage_ignores_current_policy_for_released_terminal_history(
11972 #[case] status: TransactionStatus,
11973 ) {
11974 let Some((admin_pool, pg_config)) =
11975 connect_test_postgres("protected payload terminal history").await
11976 else {
11977 return;
11978 };
11979 let schema = format!(
11980 "protected_payload_terminal_{}_{}",
11981 status.as_str(),
11982 std::process::id()
11983 );
11984 setup_execution_schema(&admin_pool, &schema).await;
11985 let options: sqlx::postgres::PgConnectOptions = pg_config.into();
11986 let database = connect_test_database(options.options([("search_path", schema.clone())]))
11987 .await
11988 .unwrap();
11989 database
11990 .ensure_execution_transaction_schema()
11991 .await
11992 .unwrap();
11993 let (intent, transaction_hash, _) = persist_test_wrap_broadcast(&database, None).await;
11994 database
11995 .record_execution_status(
11996 intent.id,
11997 &transaction_hash.to_string(),
11998 status,
11999 None,
12000 None,
12001 None,
12002 None,
12003 None,
12004 )
12005 .await
12006 .unwrap();
12007 database
12008 .mark_execution_event_emitted(intent.id, "terminal")
12009 .await
12010 .unwrap();
12011 let keys = payload_test_keys([0x62; 32], vec![], "terminal-history");
12012 database
12013 .ensure_execution_payload_storage(&keys)
12014 .await
12015 .unwrap();
12016
12017 let lease = database
12018 .require_execution_payload_storage(
12019 &keys,
12020 PayloadPolicy {
12021 chain_id: 42161,
12022 signer: Address::from_str(WALLET).unwrap(),
12023 gas_limit: 1,
12024 max_fee_per_gas: 1,
12025 },
12026 1,
12027 )
12028 .await
12029 .unwrap();
12030 drop(lease);
12031 let check = database
12032 .check_execution_payload_storage(Some(&keys), None, 1)
12033 .await
12034 .unwrap();
12035 let active_intent = reserve_test_wrap_intent(&database).await;
12036 database
12037 .assign_execution_intent_nonce(active_intent.id, 8)
12038 .await
12039 .unwrap();
12040 let active_intent = database
12041 .get_execution_intent(active_intent.id)
12042 .await
12043 .unwrap();
12044 let active_transaction = build_eip1559_transaction(
12045 42161,
12046 8,
12047 78_000,
12048 130_000_000,
12049 10_000_000,
12050 WETH_ADDRESS,
12051 U256::from(1_u64),
12052 Bytes::from(hex::decode("d0e30db0").unwrap()),
12053 );
12054 let (active_hash, active_raw_transaction) = sign_eip1559_transaction(
12055 active_transaction,
12056 &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
12057 )
12058 .await
12059 .unwrap();
12060 persist_test_payload(
12061 &database,
12062 Some(&keys),
12063 &active_intent,
12064 active_hash,
12065 &active_raw_transaction,
12066 )
12067 .await;
12068 let active_error = match database
12069 .require_execution_payload_storage(
12070 &keys,
12071 PayloadPolicy {
12072 chain_id: 42161,
12073 signer: Address::from_str(WALLET).unwrap(),
12074 gas_limit: 1,
12075 max_fee_per_gas: 1,
12076 },
12077 1,
12078 )
12079 .await
12080 {
12081 Ok(_) => panic!("active execution payload unexpectedly passed current policy"),
12082 Err(e) => e,
12083 };
12084 let active_error = format!("{active_error:#}");
12085
12086 assert!(check.protected);
12087 assert_eq!(check.authenticated_rows, 1);
12088 assert!(
12089 active_error.contains(&format!(
12090 "execution intent {} transaction {active_hash} violates current execution policy",
12091 active_intent.id
12092 )),
12093 "was: {active_error}"
12094 );
12095 assert!(
12096 active_error.contains("gas limit 78000 exceeds configured ceiling 1"),
12097 "was: {active_error}"
12098 );
12099
12100 drop_execution_schema(&admin_pool, &schema).await;
12101 }
12102
12103 #[allow(unsafe_code)] #[tokio::test]
12105 async fn rollback_blocks_execution_until_protection_and_full_check_succeed() {
12106 let state = ready_rpc_state();
12107 let Some((admin_pool, schema, mut client, state)) =
12108 execution_client_with_database("payload_rollback_reactivation", state).await
12109 else {
12110 return;
12111 };
12112 unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
12114 client.disconnect().await.unwrap();
12115 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
12116 replace_exec_event_sender(sender);
12117 client.start().unwrap();
12118
12119 client.rollback_payload_storage(1).await.unwrap();
12120 let unprotected = client.check_payload_storage(1).await.unwrap();
12121 let error = client.connect().await.unwrap_err();
12122
12123 assert!(!unprotected.protected);
12124 assert_eq!(unprotected.plaintext_rows, 0);
12125 assert_eq!(unprotected.authenticated_rows, 0);
12126 assert!(
12127 error
12128 .to_string()
12129 .contains("Postgres execution requires protected payload storage"),
12130 "was: {error}"
12131 );
12132 assert!(client.signer.is_none());
12133 assert!(state.recorded_requests().is_empty());
12134
12135 client.protect_payload_storage().await.unwrap();
12136 let protected = client.check_payload_storage(1).await.unwrap();
12137 assert!(protected.protected);
12138 assert_eq!(protected.plaintext_rows, 0);
12139 assert_eq!(protected.authenticated_rows, 0);
12140
12141 client.connect().await.unwrap();
12142
12143 assert!(client.is_connected());
12144 assert!(client.transaction_executor().is_ok());
12145 assert!(!state.recorded_requests().is_empty());
12146
12147 drop(client);
12148 drop_execution_schema(&admin_pool, &schema).await;
12149 }
12150
12151 #[rstest]
12152 #[case::active_key(true)]
12153 #[case::deployment_identity(false)]
12154 #[tokio::test]
12155 async fn postgres_connect_rejects_missing_payload_identity_before_rpc(
12156 #[case] remove_active_key: bool,
12157 ) {
12158 let test_name = if remove_active_key {
12159 "payload_connect_missing_active_key"
12160 } else {
12161 "payload_connect_missing_deployment"
12162 };
12163 let Some((admin_pool, schema, mut client, state)) =
12164 execution_client_with_database(test_name, ready_rpc_state()).await
12165 else {
12166 return;
12167 };
12168 client.disconnect().await.unwrap();
12169 client.payload_keys = None;
12170 if remove_active_key {
12171 client.config.payload_key_env = None;
12172 client.config.payload_deployment_id = None;
12173 } else {
12174 client.config.payload_deployment_id = None;
12175 }
12176
12177 let error = client.connect().await.unwrap_err();
12178
12179 assert!(
12180 error.to_string().contains(if remove_active_key {
12181 "Postgres execution requires an active payload key and deployment identity"
12182 } else {
12183 "Payload deployment ID is required"
12184 }),
12185 "was: {error}"
12186 );
12187 assert!(client.signer.is_none());
12188 assert!(client.payload_keys.is_none());
12189 assert!(state.recorded_requests().is_empty());
12190
12191 drop(client);
12192 drop_execution_schema(&admin_pool, &schema).await;
12193 }
12194
12195 #[tokio::test]
12196 async fn postgres_connect_rejects_missing_envelope_key_before_rpc() {
12197 let Some((admin_pool, schema, mut client, state)) = execution_client_with_database(
12198 "payload_connect_missing_envelope_key",
12199 ready_rpc_state(),
12200 )
12201 .await
12202 else {
12203 return;
12204 };
12205 let database = client.cache.database.as_ref().unwrap().clone();
12206 persist_test_wrap_broadcast(&database, client.payload_keys.as_deref()).await;
12207 client.disconnect().await.unwrap();
12208 client.payload_keys = None;
12209 let keys = set_test_payload_key(&mut client, [0xb6; 32], &schema);
12210 sqlx::query(sqlx::AssertSqlSafe(format!(
12211 "UPDATE {schema}.execution_payload_state SET active_key_id = $1 \
12212 WHERE component = 'signed_transactions'"
12213 )))
12214 .bind(keys.active_key_id().as_slice())
12215 .execute(&admin_pool)
12216 .await
12217 .unwrap();
12218
12219 let error = client.connect().await.unwrap_err();
12220
12221 assert!(
12222 error
12223 .to_string()
12224 .contains("Stored execution payload requires unavailable key"),
12225 "was: {error}"
12226 );
12227 assert!(client.signer.is_none());
12228 assert!(client.payload_keys.is_none());
12229 assert!(state.recorded_requests().is_empty());
12230
12231 drop(client);
12232 drop_execution_schema(&admin_pool, &schema).await;
12233 }
12234
12235 #[tokio::test]
12236 async fn payload_action_lease_blocks_rewrap_transition_until_release() {
12237 let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
12238 "payload_action_lease",
12239 execution_rpc_state(),
12240 )
12241 .await
12242 else {
12243 return;
12244 };
12245 let database = client.cache.database.as_ref().unwrap();
12246 let keys = payload_test_keys([0x71; 32], vec![], "action-lease");
12247 database
12248 .ensure_execution_payload_storage(&keys)
12249 .await
12250 .unwrap();
12251 let lease = database
12252 .acquire_execution_payload_lease(&keys)
12253 .await
12254 .unwrap();
12255 let rotated = payload_test_keys([0x72; 32], vec![[0x71; 32]], "action-lease");
12256 let mut operation = Box::pin(database.rewrap_execution_payload_storage(&rotated, 1));
12257
12258 assert!(
12259 tokio::time::timeout(Duration::from_millis(50), &mut operation)
12260 .await
12261 .is_err()
12262 );
12263 let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12264 "SELECT operation FROM {schema}.execution_payload_state"
12265 )))
12266 .fetch_one(&admin_pool)
12267 .await
12268 .unwrap();
12269 assert_eq!(state, "ready");
12270
12271 drop(lease);
12272 tokio::time::timeout(Duration::from_secs(2), operation)
12273 .await
12274 .unwrap()
12275 .unwrap();
12276 let state: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12277 "SELECT operation FROM {schema}.execution_payload_state"
12278 )))
12279 .fetch_one(&admin_pool)
12280 .await
12281 .unwrap();
12282 assert_eq!(state, "ready");
12283
12284 drop_execution_schema(&admin_pool, &schema).await;
12285 }
12286
12287 #[tokio::test]
12288 async fn payload_infrastructure_checks_are_scoped_to_the_execution_schema() {
12289 let Some((admin_pool, pg_config)) = connect_test_postgres("payload schema isolation").await
12290 else {
12291 return;
12292 };
12293 let schema_a = format!("payload_schema_a_{}", std::process::id());
12294 let schema_b = format!("payload_schema_b_{}", std::process::id());
12295 setup_execution_schema(&admin_pool, &schema_a).await;
12296 setup_execution_schema(&admin_pool, &schema_b).await;
12297 let options: sqlx::postgres::PgConnectOptions = pg_config.into();
12298 let database_a =
12299 connect_test_database(options.clone().options([("search_path", schema_a.clone())]))
12300 .await
12301 .unwrap();
12302 let database_b =
12303 connect_test_database(options.options([("search_path", schema_b.clone())]))
12304 .await
12305 .unwrap();
12306 database_a
12307 .ensure_execution_transaction_schema()
12308 .await
12309 .unwrap();
12310 database_b
12311 .ensure_execution_transaction_schema()
12312 .await
12313 .unwrap();
12314 let keys_a = payload_test_keys([0x91; 32], vec![], "schema-a");
12315 let keys_b = payload_test_keys([0x92; 32], vec![], "schema-b");
12316
12317 database_a
12318 .ensure_execution_payload_storage(&keys_a)
12319 .await
12320 .unwrap();
12321 database_b
12322 .ensure_execution_payload_storage(&keys_b)
12323 .await
12324 .unwrap();
12325
12326 for statement in [
12327 format!(
12328 "DROP TRIGGER execution_transaction_payload_fence ON \
12329 {schema_a}.execution_transaction_hash"
12330 ),
12331 format!(
12332 "ALTER TABLE {schema_a}.execution_transaction_hash \
12333 DROP CONSTRAINT execution_transaction_payload_protected_check"
12334 ),
12335 ] {
12336 sqlx::query(sqlx::AssertSqlSafe(statement))
12337 .execute(&admin_pool)
12338 .await
12339 .unwrap();
12340 }
12341
12342 let error = database_a
12343 .ensure_execution_payload_storage(&keys_a)
12344 .await
12345 .unwrap_err();
12346
12347 assert!(
12348 error
12349 .to_string()
12350 .contains("without its write fence or constraint"),
12351 "was: {error}"
12352 );
12353
12354 drop_execution_schema(&admin_pool, &schema_a).await;
12355 drop_execution_schema(&admin_pool, &schema_b).await;
12356 }
12357
12358 #[tokio::test]
12359 async fn execution_fresh_schema_preserves_nullable_wallet_address() {
12360 let Some((admin_pool, _)) = connect_test_postgres("fresh execution schema").await else {
12361 return;
12362 };
12363 let schema = format!("execution_fresh_test_{}", std::process::id());
12364 let mut transaction = admin_pool.begin().await.unwrap();
12365 sqlx::query(sqlx::AssertSqlSafe(format!("CREATE SCHEMA {schema}")))
12366 .execute(&mut *transaction)
12367 .await
12368 .unwrap();
12369 sqlx::query(sqlx::AssertSqlSafe(format!(
12370 "SET LOCAL search_path TO {schema}"
12371 )))
12372 .execute(&mut *transaction)
12373 .await
12374 .unwrap();
12375 sqlx::query("CREATE TABLE chain (chain_id INTEGER PRIMARY KEY)")
12376 .execute(&mut *transaction)
12377 .await
12378 .unwrap();
12379 sqlx::query("INSERT INTO chain (chain_id) VALUES (42161)")
12380 .execute(&mut *transaction)
12381 .await
12382 .unwrap();
12383 sqlx::query(sqlx::AssertSqlSafe(execution_transaction_create_sql()))
12384 .execute(&mut *transaction)
12385 .await
12386 .unwrap();
12387 sqlx::query(
12388 "INSERT INTO execution_transaction \
12389 (chain_id, nonce, transaction_hash, purpose, status) \
12390 VALUES (42161, 7, '0xfresh-legacy', 'wrap', 'rejected')",
12391 )
12392 .execute(&mut *transaction)
12393 .await
12394 .unwrap();
12395
12396 let is_nullable: String = sqlx::query_scalar(
12397 "SELECT is_nullable FROM information_schema.columns \
12398 WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12399 AND column_name = 'wallet_address'",
12400 )
12401 .bind(&schema)
12402 .fetch_one(&mut *transaction)
12403 .await
12404 .unwrap();
12405 let wallet_address: Option<String> = sqlx::query_scalar(
12406 "SELECT wallet_address FROM execution_transaction \
12407 WHERE transaction_hash = '0xfresh-legacy'",
12408 )
12409 .fetch_one(&mut *transaction)
12410 .await
12411 .unwrap();
12412
12413 assert_eq!(is_nullable, "YES");
12414 assert_eq!(wallet_address, None);
12415 transaction.rollback().await.unwrap();
12416 }
12417
12418 #[tokio::test]
12419 async fn execution_schema_migration_preserves_existing_rows() {
12420 let Some((admin_pool, pg_config)) =
12421 connect_test_postgres("execution schema migration").await
12422 else {
12423 return;
12424 };
12425 let schema = format!("execution_migration_test_{}", std::process::id());
12426 setup_execution_schema(&admin_pool, &schema).await;
12427 sqlx::query(sqlx::AssertSqlSafe(format!(
12428 "INSERT INTO {schema}.execution_transaction \
12429 (chain_id, nonce, transaction_hash, purpose, status) \
12430 VALUES (42161, 7, '0xlegacy', 'wrap', 'rejected')"
12431 )))
12432 .execute(&admin_pool)
12433 .await
12434 .unwrap();
12435
12436 let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12437 let db_options = db_options.options([("search_path", schema.clone())]);
12438 let database = connect_test_database(db_options).await.unwrap();
12439 database
12440 .ensure_execution_transaction_schema()
12441 .await
12442 .unwrap();
12443 let legacy = database
12444 .get_execution_transaction(42161, "0xlegacy")
12445 .await
12446 .unwrap()
12447 .unwrap();
12448 assert_eq!(legacy.purpose, "wrap");
12449 assert_eq!(legacy.status, "rejected");
12450 assert_eq!(legacy.client_order_id, None);
12451 assert_eq!(legacy.wallet_address, None);
12452 let is_nullable: String = sqlx::query_scalar(
12453 "SELECT is_nullable FROM information_schema.columns \
12454 WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12455 AND column_name = 'wallet_address'",
12456 )
12457 .bind(&schema)
12458 .fetch_one(&admin_pool)
12459 .await
12460 .unwrap();
12461 let fence_error = database
12462 .add_execution_transaction(
12463 42161,
12464 WALLET,
12465 8,
12466 "0xswap",
12467 "swap",
12468 "pending",
12469 Some("O-SWAP-001"),
12470 )
12471 .await
12472 .err()
12473 .unwrap();
12474 assert!(
12475 fence_error
12476 .to_string()
12477 .contains("Legacy execution writer refused"),
12478 "was: {fence_error}"
12479 );
12480 assert_eq!(is_nullable, "YES");
12481
12482 drop_execution_schema(&admin_pool, &schema).await;
12483 }
12484
12485 #[tokio::test]
12486 async fn execution_schema_migration_drops_legacy_wallet_not_null() {
12487 let Some((admin_pool, pg_config)) =
12488 connect_test_postgres("legacy wallet address schema migration").await
12489 else {
12490 return;
12491 };
12492 let schema = format!("execution_wallet_migration_test_{}", std::process::id());
12493 setup_execution_schema(&admin_pool, &schema).await;
12494 sqlx::query(sqlx::AssertSqlSafe(format!(
12495 "ALTER TABLE {schema}.execution_transaction \
12496 ADD COLUMN wallet_address TEXT NOT NULL"
12497 )))
12498 .execute(&admin_pool)
12499 .await
12500 .unwrap();
12501 sqlx::query(sqlx::AssertSqlSafe(format!(
12502 "INSERT INTO {schema}.execution_transaction \
12503 (chain_id, wallet_address, nonce, transaction_hash, purpose, status) \
12504 VALUES (42161, '{WALLET}', 7, '0xlegacy-wallet', 'wrap', 'rejected')"
12505 )))
12506 .execute(&admin_pool)
12507 .await
12508 .unwrap();
12509
12510 let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12511 let db_options = db_options.options([("search_path", schema.clone())]);
12512 let database = connect_test_database(db_options).await.unwrap();
12513 database
12514 .ensure_execution_transaction_schema()
12515 .await
12516 .unwrap();
12517 let legacy = database
12518 .get_execution_transaction(42161, "0xlegacy-wallet")
12519 .await
12520 .unwrap()
12521 .unwrap();
12522 let is_nullable: String = sqlx::query_scalar(
12523 "SELECT is_nullable FROM information_schema.columns \
12524 WHERE table_schema = $1 AND table_name = 'execution_transaction' \
12525 AND column_name = 'wallet_address'",
12526 )
12527 .bind(&schema)
12528 .fetch_one(&admin_pool)
12529 .await
12530 .unwrap();
12531
12532 assert_eq!(legacy.wallet_address.as_deref(), Some(WALLET));
12533 assert_eq!(is_nullable, "YES");
12534 drop_execution_schema(&admin_pool, &schema).await;
12535 }
12536
12537 #[tokio::test]
12538 async fn execution_schema_migration_refuses_unresolved_legacy_rows() {
12539 let Some((admin_pool, pg_config)) =
12540 connect_test_postgres("unsafe execution schema migration").await
12541 else {
12542 return;
12543 };
12544 let schema = format!("execution_unsafe_migration_test_{}", std::process::id());
12545 setup_execution_schema(&admin_pool, &schema).await;
12546 sqlx::query(sqlx::AssertSqlSafe(format!(
12547 "INSERT INTO {schema}.execution_transaction \
12548 (chain_id, nonce, transaction_hash, purpose, status) \
12549 VALUES (42161, 7, '0xunresolved', 'wrap', 'pending')"
12550 )))
12551 .execute(&admin_pool)
12552 .await
12553 .unwrap();
12554 let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12555 let db_options = db_options.options([("search_path", schema.clone())]);
12556 let database = connect_test_database(db_options).await.unwrap();
12557
12558 let error = database
12559 .ensure_execution_transaction_schema()
12560 .await
12561 .unwrap_err();
12562 let legacy_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
12563 "SELECT COUNT(*) FROM {schema}.execution_transaction \
12564 WHERE transaction_hash = '0xunresolved' AND status = 'pending'"
12565 )))
12566 .fetch_one(&admin_pool)
12567 .await
12568 .unwrap();
12569 let v2_table: Option<String> = sqlx::query_scalar("SELECT to_regclass($1)::TEXT")
12570 .bind(format!("{schema}.execution_intent"))
12571 .fetch_one(&admin_pool)
12572 .await
12573 .unwrap();
12574
12575 assert!(
12576 error
12577 .to_string()
12578 .contains("Cannot safely migrate 1 unresolved execution schema version 1"),
12579 "was: {error}"
12580 );
12581 assert_eq!(legacy_count, 1);
12582 assert_eq!(v2_table, None);
12583
12584 drop_execution_schema(&admin_pool, &schema).await;
12585 }
12586
12587 #[rstest]
12588 fn receipt_max_polls_derives_from_timeout() {
12589 assert_eq!(receipt_max_polls(0), 1);
12590 assert_eq!(receipt_max_polls(1), 1);
12591 assert_eq!(receipt_max_polls(60), 60);
12592 assert_eq!(receipt_max_polls(u64::MAX), u32::MAX);
12593 assert_eq!(receipt_timeout(0), Duration::from_secs(1));
12594 assert_eq!(receipt_timeout(60), Duration::from_secs(60));
12595 assert_eq!(
12596 receipt_timeout(u64::MAX),
12597 Duration::from_secs(u64::from(u32::MAX))
12598 );
12599 }
12600
12601 #[rstest]
12602 fn submit_order_errors_when_order_not_cached() {
12603 let client = swap_client_with_cache(test_config("http://127.0.0.1:1".to_string())).0;
12604 let mut cmd = submit_order_cmd(&test_market_sell_order(test_pool().instrument_id));
12605 cmd.client_order_id = ClientOrderId::from("O-UNKNOWN");
12606
12607 let error = client.submit_order(cmd).unwrap_err();
12608
12609 assert!(!error.to_string().is_empty());
12610 }
12611
12612 #[tokio::test]
12613 async fn submit_order_denies_pool_fee_above_uint24() {
12614 let cache = Rc::new(RefCell::new(Cache::default()));
12615 let mut pool = test_pool();
12616 pool.fee = Some(16_777_216); cache.borrow_mut().add_pool(pool.clone()).unwrap();
12618 let order = test_market_sell_order(pool.instrument_id);
12619 cache
12620 .borrow_mut()
12621 .add_order(order.clone(), None, None, false)
12622 .unwrap();
12623 let core = ExecutionClientCore::new(
12624 TraderId::from("TRADER-001"),
12625 ClientId::from("BLOCKCHAIN-001"),
12626 *BLOCKCHAIN_VENUE,
12627 OmsType::Netting,
12628 AccountId::from("BLOCKCHAIN-001"),
12629 AccountType::Wallet,
12630 None,
12631 cache,
12632 );
12633 let mut client =
12634 BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12635 .unwrap();
12636 let mut receiver = start_with_events(&mut client);
12637
12638 client.submit_order(submit_order_cmd(&order)).unwrap();
12639
12640 let events = collect_order_events(&mut receiver);
12641 assert_eq!(events.len(), 1);
12642 let OrderEventAny::Denied(denied) = &events[0] else {
12643 panic!("expected OrderDenied, was {:?}", events[0]);
12644 };
12645 assert!(
12646 denied.reason.contains("exceeds uint24"),
12647 "was: {}",
12648 denied.reason
12649 );
12650 }
12651
12652 #[tokio::test]
12653 async fn submit_order_denies_uninitialized_profiler() {
12654 let cache = Rc::new(RefCell::new(Cache::default()));
12655 let pool = test_pool();
12656 cache.borrow_mut().add_pool(pool.clone()).unwrap();
12657 let order = test_market_sell_order(pool.instrument_id);
12658 cache
12659 .borrow_mut()
12660 .add_order(order.clone(), None, None, false)
12661 .unwrap();
12662 cache
12663 .borrow_mut()
12664 .add_pool_profiler(PoolProfiler::new(Arc::new(pool)))
12665 .unwrap();
12666 let core = ExecutionClientCore::new(
12667 TraderId::from("TRADER-001"),
12668 ClientId::from("BLOCKCHAIN-001"),
12669 *BLOCKCHAIN_VENUE,
12670 OmsType::Netting,
12671 AccountId::from("BLOCKCHAIN-001"),
12672 AccountType::Wallet,
12673 None,
12674 cache,
12675 );
12676 let mut client =
12677 BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12678 .unwrap();
12679 let mut receiver = start_with_events(&mut client);
12680
12681 client.submit_order(submit_order_cmd(&order)).unwrap();
12682
12683 let events = collect_order_events(&mut receiver);
12684 assert_eq!(events.len(), 1);
12685 let OrderEventAny::Denied(denied) = &events[0] else {
12686 panic!("expected OrderDenied, was {:?}", events[0]);
12687 };
12688 assert!(
12689 denied.reason.contains("is not initialized"),
12690 "was: {}",
12691 denied.reason
12692 );
12693 }
12694
12695 #[tokio::test]
12696 async fn submit_order_denies_when_quote_cannot_fill_order() {
12697 let cache = Rc::new(RefCell::new(Cache::default()));
12698 let pool = test_pool();
12699 cache.borrow_mut().add_pool(pool.clone()).unwrap();
12700 let order = test_market_sell_order(pool.instrument_id);
12701 cache
12702 .borrow_mut()
12703 .add_order(order.clone(), None, None, false)
12704 .unwrap();
12705 cache
12707 .borrow_mut()
12708 .add_pool_profiler(test_profiler_with_range(
12709 &pool,
12710 FIXTURE_BLOCK,
12711 FIXTURE_BLOCK_HASH,
12712 U160::from(1u128 << 96),
12713 -10,
12714 10,
12715 1,
12716 ))
12717 .unwrap();
12718 let core = ExecutionClientCore::new(
12719 TraderId::from("TRADER-001"),
12720 ClientId::from("BLOCKCHAIN-001"),
12721 *BLOCKCHAIN_VENUE,
12722 OmsType::Netting,
12723 AccountId::from("BLOCKCHAIN-001"),
12724 AccountType::Wallet,
12725 None,
12726 cache,
12727 );
12728 let mut client =
12729 BlockchainExecutionClient::new(core, test_config("http://127.0.0.1:1".to_string()))
12730 .unwrap();
12731 let mut receiver = start_with_events(&mut client);
12732
12733 client.submit_order(submit_order_cmd(&order)).unwrap();
12734
12735 let events = collect_order_events(&mut receiver);
12736 assert_eq!(events.len(), 1);
12737 let OrderEventAny::Denied(denied) = &events[0] else {
12738 panic!("expected OrderDenied, was {:?}", events[0]);
12739 };
12740 assert!(
12741 denied.reason.contains("cannot fill the order"),
12742 "was: {}",
12743 denied.reason
12744 );
12745 }
12746
12747 #[tokio::test]
12748 async fn submit_order_applies_slippage_param_override_at_ceiling() {
12749 let Some((admin_pool, schema, mut client, state, _)) = swap_client_with_database(
12750 "execution_submit_slippage_override_test",
12751 swap_rpc_state_with_min_amount_out(expected_min_amount_out(200)).await,
12752 )
12753 .await
12754 else {
12755 return;
12756 };
12757 let order = test_market_sell_order(test_pool().instrument_id);
12758 let mut cmd = submit_order_cmd(&order);
12759 cmd.params = Some(serde_json::from_str(r#"{"slippage_bps": 200}"#).unwrap());
12761 let mut receiver = start_with_events(&mut client);
12762 let expected_min_out = expected_min_amount_out(200);
12763
12764 client.submit_order(cmd).unwrap();
12765 await_pending_tasks(&client).await;
12766
12767 let events = collect_order_events(&mut receiver);
12768 assert_swap_submitted_and_filled(&events);
12769
12770 let (_, expected_raw) = expected_swap_tx(expected_min_out).await;
12771 let broadcasts: Vec<_> = state
12772 .recorded_requests()
12773 .into_iter()
12774 .filter(|request| request["method"] == "eth_sendRawTransaction")
12775 .collect();
12776 assert_eq!(broadcasts.len(), 1);
12777 assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
12778
12779 drop_execution_schema(&admin_pool, &schema).await;
12780 }
12781
12782 #[tokio::test]
12783 async fn submit_order_accepts_quote_fresh_at_max_age_boundary() {
12784 let min_amount_out = expected_min_amount_out(50);
12785 let (tx_hash, _) = expected_swap_tx(min_amount_out).await;
12786 let head = finalized_swap_block(tx_hash, min_amount_out);
12787 let state = swap_rpc_state()
12788 .await
12789 .with_response("eth_getBlockByNumber", &head)
12790 .with_parameter_response("eth_getBlockByNumber", FIXTURE_BLOCK_PARAM, BLOCK_BY_NUMBER);
12791 let Some((admin_pool, schema, mut client, state, cache)) =
12792 swap_client_with_database_config(
12793 "execution_submit_fresh_boundary_test",
12794 state,
12795 |http_rpc_url| {
12796 let mut config = test_config(http_rpc_url);
12797 config.max_quote_age_blocks = Some(1);
12798 config
12799 },
12800 )
12801 .await
12802 else {
12803 return;
12804 };
12805 let pool = test_pool();
12806 cache
12807 .borrow_mut()
12808 .add_pool_profiler(test_profiler_at_block(
12809 &pool,
12810 FIXTURE_BLOCK,
12811 FIXTURE_BLOCK_HASH,
12812 ))
12813 .unwrap();
12814 let order = test_market_sell_order(pool.instrument_id);
12815 let mut receiver = start_with_events(&mut client);
12816
12817 client.submit_order(submit_order_cmd(&order)).unwrap();
12818 await_pending_tasks(&client).await;
12819
12820 let events = collect_order_events(&mut receiver);
12821 assert_eq!(events.len(), 1, "was: {events:?}");
12822 assert!(matches!(&events[0], OrderEventAny::Submitted(_)));
12823 assert_eq!(
12824 state
12825 .recorded_requests()
12826 .iter()
12827 .filter(|request| request["method"] == "eth_sendRawTransaction")
12828 .count(),
12829 1
12830 );
12831
12832 drop_execution_schema(&admin_pool, &schema).await;
12833 }
12834
12835 #[tokio::test]
12836 async fn submit_order_sells_base_token_from_token1_position() {
12837 let chain = Arc::new(chains::ARBITRUM.clone());
12841 let dex = Arc::clone(&UNISWAP_V3.dex);
12842 let usdc = Token::new(
12843 Arc::clone(&chain),
12844 address!("af88d065e77c8cC2239327C5EDb3A432268e5831"),
12845 "USD Coin".to_string(),
12846 "USDC".to_string(),
12847 6,
12848 );
12849 let weth = Token::new(
12850 Arc::clone(&chain),
12851 address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1"),
12852 "Wrapped Ether".to_string(),
12853 "WETH".to_string(),
12854 18,
12855 );
12856 let pool = Pool::new(
12857 chain,
12858 dex,
12859 address!("C6962004f452bE9203591991D15f6b388e09E8D0"),
12860 PoolIdentifier::from_address(address!("C6962004f452bE9203591991D15f6b388e09E8D0")),
12861 55_000_000,
12862 usdc,
12863 weth,
12864 Some(500),
12865 Some(10),
12866 UnixNanos::default(),
12867 );
12868
12869 let cache = Rc::new(RefCell::new(Cache::default()));
12870 cache.borrow_mut().add_pool(pool.clone()).unwrap();
12871 let order = test_market_sell_order(pool.instrument_id);
12872 cache
12873 .borrow_mut()
12874 .add_order(order.clone(), None, None, false)
12875 .unwrap();
12876 cache
12878 .borrow_mut()
12879 .add_pool_profiler(test_profiler_with_state(
12880 &pool,
12881 FIXTURE_BLOCK,
12882 U160::from(2u128 << 96),
12883 TEST_LIQUIDITY,
12884 ))
12885 .unwrap();
12886
12887 let (admin_pool, pg_config) = match connect_test_postgres("orientation").await {
12888 Some(setup) => setup,
12889 None => return,
12890 };
12891 let schema = format!("execution_submit_orientation_test_{}", std::process::id());
12892 setup_execution_schema(&admin_pool, &schema).await;
12893 let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
12894 let db_options = db_options.options([("search_path", schema.clone())]);
12895 let database = connect_test_database(db_options).await.unwrap();
12896
12897 let state = swap_rpc_state()
12898 .await
12899 .with_call_response(POOL_TOKEN0_SELECTOR, CALL_USDC)
12900 .with_call_response(POOL_TOKEN1_SELECTOR, CALL_WETH)
12901 .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
12902 let addr = start_mock_rpc_server(state.clone()).await;
12903 let core = ExecutionClientCore::new(
12904 TraderId::from("TRADER-001"),
12905 ClientId::from("BLOCKCHAIN-001"),
12906 *BLOCKCHAIN_VENUE,
12907 OmsType::Netting,
12908 AccountId::from("BLOCKCHAIN-001"),
12909 AccountType::Wallet,
12910 None,
12911 cache,
12912 );
12913 let mut config = test_config(format!("http://{addr}"));
12914 let result = |response: &str| {
12915 serde_json::from_str::<serde_json::Value>(response).unwrap()["result"]
12916 .as_str()
12917 .unwrap()
12918 .to_string()
12919 };
12920 {
12921 let verification = config.verification.as_mut().unwrap();
12922 let pool_identity = &mut verification.deployment_manifest.pools[0];
12923 pool_identity.token0 = USDC.to_string();
12924 pool_identity.token1 = WETH.to_string();
12925 let pool_contract = verification
12926 .deployment_manifest
12927 .contracts
12928 .iter_mut()
12929 .find(|contract| contract.role == BlockchainContractRole::Pool)
12930 .unwrap();
12931
12932 for probe in &mut pool_contract.probes {
12933 if probe.call_data.starts_with(POOL_TOKEN0_SELECTOR) {
12934 probe.expected_output = result(CALL_USDC);
12935 } else if probe.call_data.starts_with(POOL_TOKEN1_SELECTOR) {
12936 probe.expected_output = result(CALL_WETH);
12937 }
12938 }
12939 }
12940 refresh_test_manifest_digest(&mut config);
12941 let mut client = BlockchainExecutionClient::new(core, config).unwrap();
12942 client.cache.database = Some(database);
12943 client
12944 .cache
12945 .ensure_execution_transaction_schema()
12946 .await
12947 .unwrap();
12948 protect_test_storage(&mut client, &schema).await;
12949 initialize_test_verification_ledger(&client).await;
12950 client.signer = Some(Arc::new(
12951 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
12952 ));
12953 client.core.set_connected();
12954 let mut receiver = start_with_events(&mut client);
12955
12956 let profiler = test_profiler_with_state(
12957 &pool,
12958 FIXTURE_BLOCK,
12959 U160::from(2u128 << 96),
12960 TEST_LIQUIDITY,
12961 );
12962 let quote = profiler
12963 .swap_exact_in(U256::from(1_000_000_000_000_000u64), false, None)
12964 .unwrap();
12965 let quoted = exact_output_amount("e, false).unwrap();
12966 let expected_min_out = derive_min_amount_out(quoted, 50).unwrap();
12967 let wrong_direction_quote = profiler
12968 .swap_exact_in(U256::from(1_000_000_000_000_000u64), true, None)
12969 .unwrap();
12970 let wrong_direction_out = exact_output_amount(&wrong_direction_quote, true).unwrap();
12971 assert_ne!(
12972 quoted, wrong_direction_out,
12973 "the asymmetric price must make the quote direction observable"
12974 );
12975 assert_ne!(
12976 expected_min_out,
12977 expected_min_amount_out(50),
12978 "the profiler quote must remain distinct from the independent quote fixture"
12979 );
12980
12981 client.submit_order(submit_order_cmd(&order)).unwrap();
12982 await_pending_tasks(&client).await;
12983
12984 let events = collect_order_events(&mut receiver);
12985 assert_eq!(events.len(), 1);
12986 assert!(
12987 matches!(&events[0], OrderEventAny::Submitted(_)),
12988 "was: {:?}",
12989 events[0]
12990 );
12991
12992 let (_, expected_raw) = expected_swap_tx(expected_min_amount_out(50)).await;
12993 let broadcasts: Vec<_> = state
12994 .recorded_requests()
12995 .into_iter()
12996 .filter(|request| request["method"] == "eth_sendRawTransaction")
12997 .collect();
12998 assert_eq!(broadcasts.len(), 1);
12999 assert_eq!(broadcasts[0]["params"][0].as_str().unwrap(), expected_raw);
13000
13001 drop_execution_schema(&admin_pool, &schema).await;
13002 }
13003
13004 #[tokio::test]
13005 async fn submit_order_denies_on_chain_mismatch() {
13006 let state = swap_rpc_state()
13007 .await
13008 .with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13009 let Some((admin_pool, schema, mut client, state, _)) =
13010 swap_client_with_database("execution_submit_chain_mismatch_test", state).await
13011 else {
13012 return;
13013 };
13014 let order = test_market_sell_order(test_pool().instrument_id);
13015 let mut receiver = start_with_events(&mut client);
13016
13017 client.submit_order(submit_order_cmd(&order)).unwrap();
13018 await_pending_tasks(&client).await;
13019
13020 let events = collect_order_events(&mut receiver);
13021 assert_eq!(events.len(), 1);
13022 let OrderEventAny::Denied(denied) = &events[0] else {
13023 panic!("expected OrderDenied, was {:?}", events[0]);
13024 };
13025 assert!(
13026 denied
13027 .reason
13028 .contains("pre-sign chain ID verification disagreed"),
13029 "was: {}",
13030 denied.reason
13031 );
13032 let requests = state.recorded_requests();
13033 assert!(
13034 requests
13035 .iter()
13036 .all(|request| request["method"] != "eth_sendRawTransaction"),
13037 "no broadcast may follow a chain mismatch"
13038 );
13039 assert!(client.in_flight.lock().is_none());
13040
13041 drop_execution_schema(&admin_pool, &schema).await;
13042 }
13043
13044 #[tokio::test]
13045 async fn submit_order_finality_timeout_marks_dropped_and_keeps_ownership() {
13046 let receipt_release = Arc::new(tokio::sync::Semaphore::new(0));
13047 let state = swap_rpc_state()
13048 .await
13049 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
13050 .with_response_release("eth_getTransactionReceipt", Arc::clone(&receipt_release));
13051 let Some((admin_pool, schema, mut client, state, _)) =
13052 swap_client_with_database("execution_submit_inclusion_timeout_test", state).await
13053 else {
13054 return;
13055 };
13056 client.transaction_limits.receipt_timeout_secs = 1;
13057 let order = test_market_sell_order(test_pool().instrument_id);
13058 let mut receiver = start_with_events(&mut client);
13059
13060 client.submit_order(submit_order_cmd(&order)).unwrap();
13061 await_recorded_requests(&state, "eth_getTransactionReceipt", 3).await;
13062 tokio::time::timeout(Duration::from_secs(3), await_pending_tasks(&client))
13063 .await
13064 .unwrap();
13065 receipt_release.add_permits(3);
13066
13067 let events = collect_order_events(&mut receiver);
13070 assert_eq!(events.len(), 1);
13071 assert!(
13072 matches!(&events[0], OrderEventAny::Submitted(_)),
13073 "was: {:?}",
13074 events[0]
13075 );
13076
13077 let record = client
13078 .cache
13079 .get_execution_transaction(
13080 42161,
13081 &expected_swap_tx(expected_min_amount_out(50))
13082 .await
13083 .0
13084 .to_string(),
13085 )
13086 .await
13087 .unwrap()
13088 .unwrap();
13089 assert_eq!(record.status, "dropped");
13090 assert!(client.in_flight.lock().is_some());
13091
13092 drop_execution_schema(&admin_pool, &schema).await;
13093 }
13094
13095 #[tokio::test]
13096 async fn included_receipt_finality_timeout_marks_dropped_and_keeps_ownership() {
13097 let state = execution_rpc_state()
13098 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
13099 .with_response("eth_estimateGas", ESTIMATE_GAS)
13100 .with_response("eth_call", CALL_BALANCE)
13101 .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
13102 .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
13103 .with_send_raw_transaction_echo();
13104 let Some((admin_pool, schema, mut client, _)) =
13105 execution_client_with_database("execution_included_timeout_test", state).await
13106 else {
13107 return;
13108 };
13109 client.transaction_limits.receipt_timeout_secs = 1;
13110
13111 let error = client
13112 .wrap(U256::from(1_000_000_000_000_000_u64))
13113 .await
13114 .unwrap_err();
13115 let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13116 "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
13117 )))
13118 .fetch_all(&admin_pool)
13119 .await
13120 .unwrap();
13121
13122 assert!(
13123 error.to_string().contains("Timed out awaiting finality"),
13124 "was: {error}"
13125 );
13126 assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
13127 assert!(client.in_flight.lock().is_some());
13128
13129 drop_execution_schema(&admin_pool, &schema).await;
13130 }
13131
13132 #[tokio::test]
13133 async fn submit_order_single_in_flight_rejects_concurrent_swap() {
13134 let broadcast_release = Arc::new(tokio::sync::Semaphore::new(0));
13135 let state = swap_rpc_state()
13136 .await
13137 .with_response_release("eth_sendRawTransaction", Arc::clone(&broadcast_release));
13138 let Some((admin_pool, schema, mut client, state, cache)) =
13139 swap_client_with_database("execution_submit_concurrent_test", state).await
13140 else {
13141 return;
13142 };
13143 let pool = test_pool();
13144 let first = test_market_sell_order(pool.instrument_id);
13145 let second = OrderTestBuilder::new(OrderType::Market)
13146 .trader_id(TraderId::from("TRADER-001"))
13147 .strategy_id(StrategyId::from("S-001"))
13148 .instrument_id(pool.instrument_id)
13149 .client_order_id(ClientOrderId::from("O-SWAP-002"))
13150 .side(OrderSide::Sell)
13151 .quantity(Quantity::from("0.001"))
13152 .build();
13153 cache
13154 .borrow_mut()
13155 .add_order(second.clone(), None, None, false)
13156 .unwrap();
13157 let mut receiver = start_with_events(&mut client);
13158
13159 client.submit_order(submit_order_cmd(&first)).unwrap();
13160 await_recorded_requests(&state, "eth_sendRawTransaction", 1).await;
13161 client.submit_order(submit_order_cmd(&second)).unwrap();
13162 let event = tokio::time::timeout(TEST_TIMEOUT, receiver.recv())
13163 .await
13164 .unwrap()
13165 .unwrap();
13166 let ExecutionEvent::Order(OrderEventAny::Denied(denied)) = event else {
13167 panic!("expected OrderDenied, was {event:?}");
13168 };
13169 assert_eq!(denied.client_order_id, second.client_order_id());
13170 assert!(
13171 denied
13172 .reason
13173 .contains("at most one transaction can be in flight"),
13174 "was: {}",
13175 denied.reason
13176 );
13177 broadcast_release.add_permits(1);
13178 await_pending_tasks(&client).await;
13179
13180 let events = collect_order_events(&mut receiver);
13181 assert_swap_submitted_and_filled(&events);
13182
13183 let broadcasts = state
13184 .recorded_requests()
13185 .into_iter()
13186 .filter(|request| request["method"] == "eth_sendRawTransaction")
13187 .count();
13188 assert_eq!(broadcasts, 1);
13189 let row_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
13190 "SELECT COUNT(*) FROM {schema}.execution_intent"
13191 )))
13192 .fetch_one(&admin_pool)
13193 .await
13194 .unwrap();
13195 assert_eq!(row_count, 1);
13196 assert!(client.in_flight.lock().is_none());
13197
13198 drop_execution_schema(&admin_pool, &schema).await;
13199 }
13200
13201 #[tokio::test]
13202 async fn preflight_ready_when_all_checks_pass() {
13203 let (client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13204 let pool = test_pool();
13205
13206 let report = client.preflight(&pool.instrument_id).await.unwrap();
13207
13208 assert!(report.ready, "issues: {:?}", report.issues);
13209 assert!(report.issues.is_empty());
13210 assert_eq!(report.expected_chain_id, 42161);
13211 assert_eq!(report.actual_chain_id, 42161);
13212 assert!(report.chain_id_matches);
13213 assert_eq!(
13214 report.pool.address,
13215 address!("C6962004f452bE9203591991D15f6b388e09E8D0")
13216 );
13217 assert!(report.pool.has_deployed_code);
13218 assert_eq!(report.pool.fee, Some(500));
13219 assert_eq!(
13220 report.pool.base_token,
13221 address!("82aF49447D8a07e3bd95BD0d56f35241523fBab1")
13222 );
13223 assert_eq!(
13224 report.pool.quote_token,
13225 address!("af88d065e77c8cC2239327C5EDb3A432268e5831")
13226 );
13227 assert_eq!(report.routers.len(), 1);
13228 assert!(report.routers[0].has_deployed_code);
13229 assert_eq!(report.tokens.len(), 2);
13230 assert_eq!(
13231 report.tokens[0].wallet_balance,
13232 U256::from(500_000_000_000_000_000u64)
13233 );
13234 assert_eq!(
13235 report.tokens[0].router_allowances,
13236 vec![(
13237 address!("E592427A0AEce92De3Edee1F18E0157C05861564"),
13238 U256::from(1_000_000_000_000_000_000u64)
13239 )]
13240 );
13241 assert_eq!(
13242 report.native_balance_wei,
13243 U256::from(1_000_000_000_000_000_000u64)
13244 );
13245 assert_eq!(report.base_fee_per_gas_wei, 100_000_000);
13246 assert_eq!(report.max_priority_fee_per_gas_wei, 10_000_000);
13247 assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13248 assert!(report.fee_within_ceiling);
13249
13250 let requests = state.recorded_requests();
13251 for method in ["eth_getCode", "eth_call", "eth_getBalance"] {
13252 let matching: Vec<_> = requests
13253 .iter()
13254 .filter(|request| request["method"] == method)
13255 .collect();
13256 assert!(!matching.is_empty(), "method {method}");
13257 assert!(
13258 matching
13259 .iter()
13260 .all(|request| request["params"][1] == "latest"),
13261 "method {method}: {matching:?}"
13262 );
13263 }
13264 }
13265
13266 #[tokio::test]
13267 async fn preflight_not_ready_without_pool_fee() {
13268 let addr = start_mock_rpc_server(ready_rpc_state()).await;
13269 let mut pool = test_pool();
13270 pool.fee = None;
13271 let client = test_client_from_config(test_config(format!("http://{addr}")), pool.clone());
13272
13273 let report = client.preflight(&pool.instrument_id).await.unwrap();
13274
13275 assert!(!report.ready);
13276 assert_eq!(report.pool.fee, None);
13277 assert_eq!(report.issues, vec!["Pool fee tier is missing"]);
13278 }
13279
13280 #[tokio::test]
13281 async fn preflight_not_ready_on_wrong_chain() {
13282 let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
13283 let (client, _) = client_with_mock_rpc(state).await;
13284 let pool = test_pool();
13285
13286 let report = client.preflight(&pool.instrument_id).await.unwrap();
13287
13288 assert!(!report.ready);
13289 assert!(!report.chain_id_matches);
13290 assert!(
13291 report
13292 .issues
13293 .iter()
13294 .any(|issue| issue.contains("Chain ID mismatch"))
13295 );
13296 }
13297
13298 #[tokio::test]
13299 async fn preflight_not_ready_without_deployed_code() {
13300 let state = ready_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13301 let (client, _) = client_with_mock_rpc(state).await;
13302 let pool = test_pool();
13303
13304 let report = client.preflight(&pool.instrument_id).await.unwrap();
13305
13306 assert!(!report.ready);
13307 assert!(!report.pool.has_deployed_code);
13308 assert!(!report.routers[0].has_deployed_code);
13309 assert!(report.tokens.iter().all(|t| !t.has_deployed_code));
13310 assert!(
13311 report
13312 .issues
13313 .iter()
13314 .any(|issue| issue.contains("No deployed bytecode at router address"))
13315 );
13316 }
13317
13318 #[tokio::test]
13319 async fn preflight_not_ready_with_zero_balances_and_allowance() {
13320 let state = ready_rpc_state()
13321 .with_response("eth_getBalance", GET_BALANCE_ZERO)
13322 .with_call_response(BALANCE_OF_SELECTOR, CALL_ZERO)
13323 .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
13324 let (client, _) = client_with_mock_rpc(state).await;
13325 let pool = test_pool();
13326
13327 let report = client.preflight(&pool.instrument_id).await.unwrap();
13328
13329 assert!(!report.ready);
13330 assert!(
13331 report
13332 .issues
13333 .iter()
13334 .any(|issue| issue.contains("Native currency balance is zero"))
13335 );
13336 assert!(
13337 report
13338 .issues
13339 .iter()
13340 .any(|issue| issue.contains("balance is zero"))
13341 );
13342 assert!(
13343 report
13344 .issues
13345 .iter()
13346 .any(|issue| issue.contains("No router allowance"))
13347 );
13348 }
13349
13350 #[tokio::test]
13351 async fn preflight_not_ready_when_fees_exceed_ceiling() {
13352 let addr = start_mock_rpc_server(ready_rpc_state()).await;
13353 let mut config = test_config(format!("http://{addr}"));
13354 config.max_fee_per_gas_wei = 1;
13355 let pool = test_pool();
13356 let client = test_client_from_config(config, pool.clone());
13357
13358 let report = client.preflight(&pool.instrument_id).await.unwrap();
13359
13360 assert!(!report.ready);
13361 assert!(!report.fee_within_ceiling);
13362 assert_eq!(report.derived_max_fee_per_gas_wei, 130_000_000);
13363 assert!(
13364 report
13365 .issues
13366 .iter()
13367 .any(|issue| issue.contains("exceeds ceiling"))
13368 );
13369 }
13370
13371 #[rstest]
13372 fn resolve_pool_rejects_unknown_pool() {
13373 let client = test_client("http://127.0.0.1:1".to_string());
13374 let unknown: InstrumentId = "0x68b3465833fb72A70ecDF485E0e4C7bD8665Fc45.Arbitrum:UniswapV3"
13375 .parse()
13376 .unwrap();
13377
13378 let error = client.resolve_pool(&unknown).unwrap_err();
13379
13380 assert!(error.to_string().contains("Unknown pool"), "was: {error}");
13381 }
13382
13383 #[rstest]
13384 fn resolve_pool_rejects_mismatched_chain() {
13385 let client = test_client("http://127.0.0.1:1".to_string());
13386 let ethereum_pool: InstrumentId =
13387 "0xC6962004f452bE9203591991D15f6b388e09E8D0.Ethereum:UniswapV3"
13388 .parse()
13389 .unwrap();
13390
13391 let error = client.resolve_pool(ðereum_pool).unwrap_err();
13392
13393 assert!(
13394 error
13395 .to_string()
13396 .contains("does not match the client chain"),
13397 "was: {error}"
13398 );
13399 }
13400
13401 #[rstest]
13402 fn resolve_pool_rejects_unsupported_dex() {
13403 let client = test_client("http://127.0.0.1:1".to_string());
13404 let v4_pool: InstrumentId = "0xC6962004f452bE9203591991D15f6b388e09E8D0.Arbitrum:UniswapV4"
13405 .parse()
13406 .unwrap();
13407
13408 let error = client.resolve_pool(&v4_pool).unwrap_err();
13409
13410 assert!(
13411 error.to_string().contains("only UniswapV3 is supported"),
13412 "was: {error}"
13413 );
13414 }
13415
13416 #[rstest]
13417 fn resolve_pool_rejects_pool_id_identifier() {
13418 let client = test_client("http://127.0.0.1:1".to_string());
13419 let pool_id: InstrumentId =
13420 "0x0000000000000000000000000000000000000000000000000000000000000000.Arbitrum:UniswapV3"
13421 .parse()
13422 .unwrap();
13423
13424 let error = client.resolve_pool(&pool_id).unwrap_err();
13425
13426 assert!(
13427 error
13428 .to_string()
13429 .contains("only address identifiers are supported"),
13430 "was: {error}"
13431 );
13432 }
13433
13434 #[rstest]
13435 fn resolve_pool_rejects_ambiguous_token_priority() {
13436 let chain = Arc::new(chains::ARBITRUM.clone());
13437 let dex = Arc::clone(&UNISWAP_V3.dex);
13438 let token_a = Token::new(
13439 Arc::clone(&chain),
13440 address!("1111111111111111111111111111111111111111"),
13441 "Token A".to_string(),
13442 "TOKA".to_string(),
13443 18,
13444 );
13445 let token_b = Token::new(
13446 Arc::clone(&chain),
13447 address!("2222222222222222222222222222222222222222"),
13448 "Token B".to_string(),
13449 "TOKB".to_string(),
13450 18,
13451 );
13452 let pool = Pool::new(
13453 chain,
13454 dex,
13455 address!("3333333333333333333333333333333333333333"),
13456 PoolIdentifier::from_address(address!("3333333333333333333333333333333333333333")),
13457 55_000_000,
13458 token_a,
13459 token_b,
13460 Some(500),
13461 Some(10),
13462 UnixNanos::default(),
13463 );
13464 let client =
13465 test_client_from_config(test_config("http://127.0.0.1:1".to_string()), pool.clone());
13466
13467 let error = client.resolve_pool(&pool.instrument_id).unwrap_err();
13468
13469 assert!(error.to_string().contains("ambiguous"), "was: {error}");
13470 }
13471
13472 #[rstest]
13473 fn new_parses_pair_specific_quote_spend_limits_with_distinct_precisions() {
13474 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13475 config.quote_spend_limits = Some(vec![
13476 quote_spend_limit(WETH, USDC, 18, &U256::MAX.to_string()),
13477 quote_spend_limit(USDC, WETH, 6, "1000000000"),
13478 ]);
13479
13480 let client = test_client_from_config(config, test_pool());
13481 let sell_ceiling = client
13482 .transaction_limits
13483 .quote_spend_limits
13484 .get(&(WETH_ADDRESS, USDC_ADDRESS))
13485 .unwrap();
13486 let buy_ceiling = client
13487 .transaction_limits
13488 .quote_spend_limits
13489 .get(&(USDC_ADDRESS, WETH_ADDRESS))
13490 .unwrap();
13491
13492 assert_eq!(sell_ceiling.spend_token, WETH_ADDRESS);
13493 assert_eq!(sell_ceiling.spend_token_decimals, 18);
13494 assert_eq!(sell_ceiling.max_amount, U256::MAX);
13495 assert_eq!(buy_ceiling.spend_token, USDC_ADDRESS);
13496 assert_eq!(buy_ceiling.spend_token_decimals, 6);
13497 assert_eq!(buy_ceiling.max_amount, U256::from(1_000_000_000u64));
13498 }
13499
13500 #[rstest]
13501 fn new_rejects_quote_spend_limit_token_pair_mismatch() {
13502 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13503 config.quote_spend_limits.as_mut().unwrap()[0].spend_token = WETH.to_string();
13504
13505 let error = test_client_result(config, test_pool()).unwrap_err();
13506
13507 assert!(
13508 error
13509 .to_string()
13510 .contains("`spend_token` must match `token_in`"),
13511 "was: {error}"
13512 );
13513 }
13514
13515 #[rstest]
13516 fn new_rejects_quote_spend_limit_pair_outside_allowlist() {
13517 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13518 config.quote_spend_limits = Some(vec![quote_spend_limit(
13519 USDC,
13520 "0x1111111111111111111111111111111111111111",
13521 6,
13522 "1000000000",
13523 )]);
13524
13525 let error = test_client_result(config, test_pool()).unwrap_err();
13526
13527 assert!(
13528 error
13529 .to_string()
13530 .contains("is not in the `allowed_token_pairs` allowlist"),
13531 "was: {error}"
13532 );
13533 }
13534
13535 #[rstest]
13536 fn new_rejects_duplicate_quote_spend_pairs() {
13537 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13538 config.quote_spend_limits = Some(vec![
13539 quote_spend_limit(USDC, WETH, 6, "1000000000"),
13540 quote_spend_limit(USDC, WETH, 6, "2000000000"),
13541 ]);
13542
13543 let error = test_client_result(config, test_pool()).unwrap_err();
13544
13545 assert!(
13546 error
13547 .to_string()
13548 .contains("Duplicate quote spend limit for token pair"),
13549 "was: {error}"
13550 );
13551 }
13552
13553 #[rstest]
13554 #[case::empty("")]
13555 #[case::signed("-1")]
13556 #[case::fractional("1.5")]
13557 #[case::hexadecimal("0x10")]
13558 #[case::overflow(
13559 "115792089237316195423570985008687907853269984665640564039457584007913129639936"
13560 )]
13561 fn new_rejects_invalid_quote_spend_max_amount(#[case] max_amount: &str) {
13562 let mut config = buy_test_config("http://127.0.0.1:1".to_string());
13563 config.quote_spend_limits.as_mut().unwrap()[0].max_amount = max_amount.to_string();
13564
13565 let error = test_client_result(config, test_pool()).unwrap_err();
13566
13567 assert!(
13568 error.to_string().contains("Quote spend limit `max_amount`"),
13569 "was: {error}"
13570 );
13571 }
13572
13573 #[rstest]
13574 #[case::allowed_token_pairs("allowed_token_pairs")]
13575 #[case::slippage_bps("slippage_bps")]
13576 #[case::max_slippage_bps("max_slippage_bps")]
13577 #[case::max_order_amount("max_order_amount")]
13578 #[case::deadline_seconds("deadline_seconds")]
13579 #[case::max_quote_age_blocks("max_quote_age_blocks")]
13580 #[case::receipt_timeout_secs("receipt_timeout_secs")]
13581 fn new_rejects_each_missing_transaction_limit(#[case] missing: &str) {
13582 let mut config = test_config("http://127.0.0.1:1".to_string());
13583 match missing {
13584 "allowed_token_pairs" => config.allowed_token_pairs = None,
13585 "slippage_bps" => config.slippage_bps = None,
13586 "max_slippage_bps" => config.max_slippage_bps = None,
13587 "max_order_amount" => config.max_order_amount = None,
13588 "deadline_seconds" => config.deadline_seconds = None,
13589 "max_quote_age_blocks" => config.max_quote_age_blocks = None,
13590 "receipt_timeout_secs" => config.receipt_timeout_secs = None,
13591 _ => unreachable!(),
13592 }
13593
13594 let error = test_client_result(config, test_pool()).unwrap_err();
13595
13596 assert_eq!(
13597 error.to_string(),
13598 "Blockchain execution transaction limits are required: allowed_token_pairs, slippage_bps, max_slippage_bps, max_order_amount, deadline_seconds, max_quote_age_blocks, receipt_timeout_secs"
13599 );
13600 }
13601
13602 #[rstest]
13603 fn new_rejects_empty_router_allowlist() {
13604 let mut config = test_config("http://127.0.0.1:1".to_string());
13605 config.router_addresses = Vec::new();
13606
13607 let error = test_client_result(config, test_pool()).unwrap_err();
13608
13609 assert!(
13610 error.to_string().contains("at least one router address"),
13611 "was: {error}"
13612 );
13613 }
13614
13615 #[tokio::test]
13616 async fn wrap_refuses_without_durable_store() {
13617 let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13618 client.core.set_connected();
13619
13620 let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13621
13622 assert!(
13623 error.to_string().contains("No durable store configured"),
13624 "was: {error}"
13625 );
13626 }
13627
13628 #[tokio::test]
13629 async fn approve_rejects_router_outside_allowlist() {
13630 let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13631
13632 let error = client
13633 .approve(
13634 WETH_ADDRESS,
13635 U256::from(1_000u64),
13636 address!("68b3465833fb72A70ecDF485E0e4C7bD8665Fc45"),
13637 )
13638 .await
13639 .err()
13640 .unwrap();
13641
13642 assert!(
13643 error
13644 .to_string()
13645 .contains("not in the configured `router_addresses` allowlist"),
13646 "was: {error}"
13647 );
13648 assert!(state.recorded_requests().is_empty());
13649 }
13650
13651 #[tokio::test]
13652 async fn approve_rejects_token_outside_input_allowlist() {
13653 let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
13654
13655 let error = client
13656 .approve(USDC_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13657 .await
13658 .unwrap_err();
13659
13660 assert!(
13661 error
13662 .to_string()
13663 .contains("is not an input token in the configured `allowed_token_pairs`"),
13664 "was: {error}"
13665 );
13666 assert!(state.recorded_requests().is_empty());
13667 }
13668
13669 #[tokio::test]
13670 async fn in_flight_guard_rejects_second_transaction() {
13671 let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13672 client.core.set_connected();
13673 *client.in_flight.lock() = Some(InFlightSlot::AwaitingFinality(InFlightTransaction {
13674 intent_id: 1,
13675 nonce: 7,
13676 tx_hash: B256::ZERO,
13677 purpose: TransactionPurpose::Wrap,
13678 }));
13679
13680 let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
13681
13682 assert!(
13683 error.to_string().contains("still awaiting finality"),
13684 "was: {error}"
13685 );
13686 }
13687
13688 #[tokio::test]
13689 async fn wrap_rejects_zero_amount() {
13690 let (mut client, _) = client_with_mock_rpc(ready_rpc_state()).await;
13691
13692 let error = client.wrap(U256::ZERO).await.unwrap_err();
13693
13694 assert!(
13695 error.to_string().contains("Wrap amount must be positive"),
13696 "was: {error}"
13697 );
13698 }
13699
13700 #[tokio::test]
13701 async fn wrap_rejects_code_free_target_before_broadcast() {
13702 let state = execution_rpc_state().with_response("eth_getCode", GET_CODE_EMPTY);
13703 let Some((admin_pool, schema, mut client, state)) =
13704 execution_client_with_database("execution_wrap_code_free_test", state).await
13705 else {
13706 return;
13707 };
13708
13709 let error = client
13710 .wrap(U256::from(1_000_000_000_000_000u64))
13711 .await
13712 .unwrap_err();
13713
13714 assert!(
13715 error
13716 .to_string()
13717 .contains("pre-sign deployment manifest verification disagreed"),
13718 "was: {error}"
13719 );
13720 assert!(client.in_flight.lock().is_none());
13721 let requests = state.recorded_requests();
13722 assert_eq!(
13723 requests
13724 .iter()
13725 .filter(|request| request["method"] == "eth_getCode")
13726 .count(),
13727 3
13728 );
13729 assert!(
13730 requests
13731 .iter()
13732 .all(|request| request["method"] != "eth_sendRawTransaction")
13733 );
13734
13735 drop_execution_schema(&admin_pool, &schema).await;
13736 }
13737
13738 #[tokio::test]
13739 async fn wrap_rejects_unrelated_target_before_broadcast() {
13740 let state = execution_rpc_state().with_response("eth_call", CALL_EMPTY);
13741 let Some((admin_pool, schema, mut client, state)) =
13742 execution_client_with_database("execution_wrap_unrelated_test", state).await
13743 else {
13744 return;
13745 };
13746
13747 let error = client
13748 .wrap(U256::from(1_000_000_000_000_000u64))
13749 .await
13750 .unwrap_err();
13751
13752 assert!(
13753 error
13754 .to_string()
13755 .contains("pre-sign wrapped token probe verification is unavailable"),
13756 "was: {error}"
13757 );
13758 assert!(client.in_flight.lock().is_none());
13759 let requests = state.recorded_requests();
13760 assert!(
13761 requests
13762 .iter()
13763 .all(|request| request["method"] != "eth_sendRawTransaction")
13764 );
13765
13766 drop_execution_schema(&admin_pool, &schema).await;
13767 }
13768
13769 #[tokio::test]
13770 async fn wrap_rejects_included_transaction_without_balance_delta() {
13771 let state = broadcast_rpc_state().with_response_sequence("eth_call", &[CALL_BALANCE; 9]);
13772 let Some((admin_pool, schema, mut client, state)) =
13773 execution_client_with_database("execution_wrap_no_delta_test", state).await
13774 else {
13775 return;
13776 };
13777
13778 let error = client
13779 .wrap(U256::from(1_000_000_000_000_000u64))
13780 .await
13781 .unwrap_err();
13782
13783 assert!(
13784 error.to_string().contains("did not increase"),
13785 "was: {error}"
13786 );
13787 let in_flight = awaiting_in_flight(&client);
13788 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13789 assert_eq!(
13790 execution_intent_markers(&admin_pool, &schema).await,
13791 vec![("wrap".into(), "broadcast".into(), false, true)]
13792 );
13793 let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13794 "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13795 )))
13796 .fetch_one(&admin_pool)
13797 .await
13798 .unwrap();
13799 assert_eq!(nonce_state, (7, 0));
13800 let broadcasts = state
13801 .recorded_requests()
13802 .into_iter()
13803 .filter(|request| request["method"] == "eth_sendRawTransaction")
13804 .count();
13805 assert_eq!(broadcasts, 1);
13806
13807 let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
13808 let block = finalized_wrap_block(expected_hash);
13809 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
13810 let restart_state = with_finalized_identity(
13811 execution_rpc_state()
13812 .with_response("eth_getTransactionReceipt", &receipt)
13813 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
13814 .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
13815 &block,
13816 &receipt,
13817 );
13818 let addr = start_mock_rpc_server(restart_state).await;
13819 let error = later_reconnect(client, format!("http://{addr}")).await;
13820 assert!(
13821 error.to_string().contains("did not increase"),
13822 "was: {error}"
13823 );
13824 assert_eq!(
13825 execution_intent_markers(&admin_pool, &schema).await,
13826 vec![("wrap".into(), "broadcast".into(), false, true)]
13827 );
13828 let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
13829 "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
13830 )))
13831 .fetch_one(&admin_pool)
13832 .await
13833 .unwrap();
13834 assert_eq!(nonce_state, (7, 0));
13835
13836 drop_execution_schema(&admin_pool, &schema).await;
13837 }
13838
13839 #[tokio::test]
13840 async fn wrap_reports_inclusion_when_postcondition_read_fails() {
13841 let state = broadcast_rpc_state().with_response_sequence(
13842 "eth_call",
13843 &[
13844 CALL_BALANCE,
13845 CALL_BALANCE,
13846 CALL_BALANCE,
13847 CALL_BALANCE,
13848 CALL_BALANCE,
13849 CALL_BALANCE,
13850 RPC_METHOD_NOT_FOUND,
13851 RPC_METHOD_NOT_FOUND,
13852 RPC_METHOD_NOT_FOUND,
13853 ],
13854 );
13855 let Some((admin_pool, schema, mut client, _)) =
13856 execution_client_with_database("execution_wrap_postcondition_rpc_test", state).await
13857 else {
13858 return;
13859 };
13860
13861 let error = client
13862 .wrap(U256::from(1_000_000_000_000_000u64))
13863 .await
13864 .unwrap_err();
13865
13866 let message = error.to_string();
13867 assert!(
13868 message.contains("failed to verify WETH balance after included transaction 0x"),
13869 "was: {message}"
13870 );
13871 assert!(message.contains("at block 30346561"), "was: {message}");
13872 let in_flight = awaiting_in_flight(&client);
13873 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
13874 assert_eq!(
13875 execution_intent_markers(&admin_pool, &schema).await,
13876 vec![("wrap".into(), "broadcast".into(), false, true)]
13877 );
13878
13879 drop_execution_schema(&admin_pool, &schema).await;
13880 }
13881
13882 #[tokio::test]
13883 async fn approve_rejects_false_return_before_broadcast() {
13884 let state = execution_rpc_state().with_response("eth_call", CALL_ZERO);
13885 let Some((admin_pool, schema, mut client, state)) =
13886 execution_client_with_database("execution_approve_false_test", state).await
13887 else {
13888 return;
13889 };
13890
13891 let error = client
13892 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13893 .await
13894 .unwrap_err();
13895
13896 assert!(error.to_string().contains("returned false"), "was: {error}");
13897 assert!(client.in_flight.lock().is_none());
13898 let requests = state.recorded_requests();
13899 let approval_calls = requests
13900 .iter()
13901 .filter(|request| {
13902 request["method"] == "eth_call"
13903 && request["params"][0]["data"]
13904 .as_str()
13905 .is_some_and(|data| data.starts_with("0x095ea7b3"))
13906 })
13907 .collect::<Vec<_>>();
13908 assert_eq!(approval_calls.len(), 3);
13909 for request in approval_calls {
13910 assert_eq!(
13911 request["params"][0]["from"]
13912 .as_str()
13913 .unwrap()
13914 .parse::<Address>()
13915 .unwrap(),
13916 WALLET.parse::<Address>().unwrap()
13917 );
13918 assert_eq!(request["params"][1], FIXTURE_BLOCK_PARAM);
13919 }
13920 assert!(
13921 requests
13922 .iter()
13923 .all(|request| request["method"] != "eth_getTransactionCount")
13924 );
13925 assert!(
13926 requests
13927 .iter()
13928 .all(|request| request["method"] != "eth_sendRawTransaction")
13929 );
13930
13931 drop_execution_schema(&admin_pool, &schema).await;
13932 }
13933
13934 #[tokio::test]
13935 async fn approve_rejects_router_with_wrong_factory_before_signing() {
13936 let state = ready_rpc_state().with_call_response(FACTORY_SELECTOR, CALL_ZERO);
13937 let Some((admin_pool, schema, mut client, state)) =
13938 execution_client_with_database("execution_approve_factory_test", state).await
13939 else {
13940 return;
13941 };
13942
13943 let error = client
13944 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13945 .await
13946 .unwrap_err();
13947
13948 assert!(
13949 error
13950 .to_string()
13951 .contains("pre-sign deployment manifest verification disagreed"),
13952 "was: {error}"
13953 );
13954 let requests = state.recorded_requests();
13955 assert!(
13956 requests
13957 .iter()
13958 .all(|request| request["method"] != "eth_getTransactionCount")
13959 );
13960 assert!(
13961 requests
13962 .iter()
13963 .all(|request| request["method"] != "eth_sendRawTransaction")
13964 );
13965
13966 drop_execution_schema(&admin_pool, &schema).await;
13967 }
13968
13969 #[tokio::test]
13970 async fn approve_rejects_router_with_wrong_weth_before_signing() {
13971 let state = ready_rpc_state().with_call_response(WETH9_SELECTOR, CALL_ZERO);
13972 let Some((admin_pool, schema, mut client, state)) =
13973 execution_client_with_database("execution_approve_weth_test", state).await
13974 else {
13975 return;
13976 };
13977
13978 let error = client
13979 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
13980 .await
13981 .unwrap_err();
13982
13983 assert!(
13984 error
13985 .to_string()
13986 .contains("pre-sign deployment manifest verification disagreed"),
13987 "was: {error}"
13988 );
13989 let requests = state.recorded_requests();
13990 assert!(
13991 requests
13992 .iter()
13993 .all(|request| request["method"] != "eth_getTransactionCount")
13994 );
13995 assert!(
13996 requests
13997 .iter()
13998 .all(|request| request["method"] != "eth_sendRawTransaction")
13999 );
14000
14001 drop_execution_schema(&admin_pool, &schema).await;
14002 }
14003
14004 #[tokio::test]
14005 async fn approve_rejects_nonzero_to_nonzero_transition_before_signing() {
14006 let state = ready_rpc_state();
14007 let Some((admin_pool, schema, mut client, state)) =
14008 execution_client_with_database("execution_approve_nonzero_test", state).await
14009 else {
14010 return;
14011 };
14012
14013 let error = client
14014 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14015 .await
14016 .unwrap_err();
14017
14018 assert!(
14019 error.to_string().contains("approve zero before setting"),
14020 "was: {error}"
14021 );
14022 let requests = state.recorded_requests();
14023 assert!(
14024 requests
14025 .iter()
14026 .all(|request| request["method"] != "eth_getTransactionCount")
14027 );
14028 assert!(
14029 requests
14030 .iter()
14031 .all(|request| request["method"] != "eth_sendRawTransaction")
14032 );
14033
14034 drop_execution_schema(&admin_pool, &schema).await;
14035 }
14036
14037 #[tokio::test]
14038 async fn approve_zero_revokes_under_unlimited_policy() {
14039 let state = broadcast_rpc_state()
14040 .with_response("eth_call", CALL_BOOL_TRUE)
14041 .with_call_response_sequence(
14042 ALLOWANCE_SELECTOR,
14043 &[
14044 CALL_ALLOWANCE,
14045 CALL_ALLOWANCE,
14046 CALL_ALLOWANCE,
14047 CALL_ZERO,
14048 CALL_ZERO,
14049 CALL_ZERO,
14050 ],
14051 );
14052 let Some((admin_pool, schema, mut client, state)) =
14053 execution_client_with_database("execution_approve_revoke_test", state).await
14054 else {
14055 return;
14056 };
14057 client.config.unlimited_approval = true;
14058 client.transaction_limits.receipt_timeout_secs = TEST_TIMEOUT.as_secs();
14059
14060 let tx_hash = client
14061 .approve(WETH_ADDRESS, U256::ZERO, ROUTER_ADDRESS)
14062 .await
14063 .unwrap();
14064
14065 assert_eq!(tx_hash, expected_approve_tx_hash(U256::ZERO).await);
14066 let approve_data = state
14067 .recorded_requests()
14068 .into_iter()
14069 .find_map(|request| {
14070 (request["method"] == "eth_estimateGas")
14071 .then(|| request["params"][0]["data"].as_str().map(str::to_owned))
14072 .flatten()
14073 })
14074 .unwrap();
14075 assert!(approve_data.starts_with("0x095ea7b3"));
14076 assert!(approve_data.ends_with(&"0".repeat(64)));
14077 assert_eq!(
14078 state
14079 .recorded_requests()
14080 .iter()
14081 .filter(|request| {
14082 request["method"] == "eth_call"
14083 && request["params"][0]["data"]
14084 .as_str()
14085 .is_some_and(|data| data.starts_with(FACTORY_SELECTOR))
14086 })
14087 .count(),
14088 12
14089 );
14090 assert_eq!(
14091 execution_intent_markers(&admin_pool, &schema).await,
14092 vec![("approve".into(), "finalized".into(), true, false)]
14093 );
14094
14095 drop_execution_schema(&admin_pool, &schema).await;
14096 }
14097
14098 #[tokio::test]
14099 async fn approve_accepts_empty_return_with_sufficient_allowance() {
14100 let state = broadcast_rpc_state()
14101 .with_response("eth_call", CALL_EMPTY)
14102 .with_call_response_sequence(
14103 ALLOWANCE_SELECTOR,
14104 &[
14105 CALL_ZERO,
14106 CALL_ZERO,
14107 CALL_ZERO,
14108 CALL_ALLOWANCE_1000,
14109 CALL_ALLOWANCE_1000,
14110 CALL_ALLOWANCE_1000,
14111 ],
14112 );
14113 let Some((admin_pool, schema, mut client, _)) =
14114 execution_client_with_database("execution_approve_empty_test", state).await
14115 else {
14116 return;
14117 };
14118
14119 let tx_hash = client
14120 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14121 .await
14122 .unwrap();
14123
14124 let record = client
14125 .cache
14126 .get_execution_transaction(42161, &tx_hash.to_string())
14127 .await
14128 .unwrap()
14129 .unwrap();
14130 assert_eq!(record.purpose, "approve");
14131 assert_eq!(record.status, "finalized");
14132 assert!(client.in_flight.lock().is_none());
14133 assert_eq!(
14134 execution_intent_markers(&admin_pool, &schema).await,
14135 vec![("approve".into(), "finalized".into(), true, false)]
14136 );
14137
14138 drop_execution_schema(&admin_pool, &schema).await;
14139 }
14140
14141 #[tokio::test]
14142 async fn approve_rejects_empty_return_with_insufficient_allowance() {
14143 let state = broadcast_rpc_state()
14144 .with_response("eth_call", CALL_EMPTY)
14145 .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 6]);
14146 let Some((admin_pool, schema, mut client, _)) =
14147 execution_client_with_database("execution_approve_insufficient_test", state).await
14148 else {
14149 return;
14150 };
14151
14152 let error = client
14153 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14154 .await
14155 .unwrap_err();
14156
14157 assert!(
14158 error
14159 .to_string()
14160 .contains("does not equal the requested amount"),
14161 "was: {error}"
14162 );
14163 let in_flight = awaiting_in_flight(&client);
14164 assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14165 assert_eq!(
14166 execution_intent_markers(&admin_pool, &schema).await,
14167 vec![("approve".into(), "broadcast".into(), false, true)]
14168 );
14169 let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14170 "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14171 )))
14172 .fetch_one(&admin_pool)
14173 .await
14174 .unwrap();
14175 assert_eq!(nonce_state, (7, 0));
14176
14177 let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
14178 let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
14179 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
14180 let restart_state = with_finalized_identity(
14181 execution_rpc_state()
14182 .with_response("eth_getTransactionReceipt", &receipt)
14183 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
14184 .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
14185 &block,
14186 &receipt,
14187 );
14188 let addr = start_mock_rpc_server(restart_state).await;
14189 let error = later_reconnect(client, format!("http://{addr}")).await;
14190 assert!(
14191 error
14192 .to_string()
14193 .contains("does not equal the requested amount"),
14194 "was: {error}"
14195 );
14196 assert_eq!(
14197 execution_intent_markers(&admin_pool, &schema).await,
14198 vec![("approve".into(), "broadcast".into(), false, true)]
14199 );
14200 let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
14201 "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
14202 )))
14203 .fetch_one(&admin_pool)
14204 .await
14205 .unwrap();
14206 assert_eq!(nonce_state, (7, 0));
14207
14208 drop_execution_schema(&admin_pool, &schema).await;
14209 }
14210
14211 #[tokio::test]
14212 async fn approve_reports_inclusion_when_postcondition_read_fails() {
14213 let state = broadcast_rpc_state()
14214 .with_response("eth_call", CALL_BOOL_TRUE)
14215 .with_call_response_sequence(
14216 ALLOWANCE_SELECTOR,
14217 &[
14218 CALL_ZERO,
14219 CALL_ZERO,
14220 CALL_ZERO,
14221 RPC_METHOD_NOT_FOUND,
14222 RPC_METHOD_NOT_FOUND,
14223 RPC_METHOD_NOT_FOUND,
14224 ],
14225 );
14226 let Some((admin_pool, schema, mut client, _)) =
14227 execution_client_with_database("execution_approve_postcondition_rpc_test", state).await
14228 else {
14229 return;
14230 };
14231
14232 let error = client
14233 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
14234 .await
14235 .unwrap_err();
14236
14237 let message = error.to_string();
14238 assert!(
14239 message.contains("failed to verify router allowance after included transaction 0x"),
14240 "was: {message}"
14241 );
14242 assert!(message.contains("at block 30346561"), "was: {message}");
14243 let in_flight = awaiting_in_flight(&client);
14244 assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
14245 assert_eq!(
14246 execution_intent_markers(&admin_pool, &schema).await,
14247 vec![("approve".into(), "broadcast".into(), false, true)]
14248 );
14249
14250 drop_execution_schema(&admin_pool, &schema).await;
14251 }
14252
14253 #[tokio::test]
14254 async fn wallet_balance_refresh_replaces_complete_snapshot_with_exact_precision() {
14255 let state = execution_rpc_state()
14256 .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14257 .with_response_sequence(
14258 "eth_call",
14259 &[
14260 CALL_BALANCE_WETH,
14261 CALL_BALANCE_USDC,
14262 CALL_BALANCE_WETH_UPDATED,
14263 CALL_BALANCE_USDC_UPDATED,
14264 ],
14265 );
14266 let (mut client, _, _) =
14267 client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_REPLACE").await;
14268 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14269 replace_exec_event_sender(sender);
14270 client.start().unwrap();
14271
14272 client.refresh_wallet_balances().await.unwrap();
14273 let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14274
14275 assert_eq!(balances.len(), 3);
14276 assert_eq!(balances[0].currency.code, "ETH");
14277 assert_eq!(balances[0].currency.name, "Ethereum");
14278 assert_eq!(balances[0].currency.precision, 18);
14279 assert_eq!(balances[0].total.raw(), 1_000_000_000_000_000_000);
14280 assert_eq!(balances[0].free, balances[0].total);
14281 assert_eq!(balances[0].locked, Money::zero(balances[0].currency));
14282 assert_eq!(balances[1].currency.code, "WETH");
14283 assert_eq!(balances[1].currency.name, "Wrapped Ether");
14284 assert_eq!(balances[1].currency.precision, 18);
14285 assert_eq!(balances[1].total.raw(), 1_234_567_890_123_456_789);
14286 assert_eq!(balances[1].free, balances[1].total);
14287 assert_eq!(balances[1].locked, Money::zero(balances[1].currency));
14288 assert_eq!(balances[2].currency.code, "USDC");
14289 assert_eq!(balances[2].currency.name, "USD Coin");
14290 assert_eq!(balances[2].currency.precision, 6);
14291 assert_eq!(balances[2].total.raw(), 9_876_543_210_000_000_000);
14292 assert_eq!(balances[2].free, balances[2].total);
14293 assert_eq!(balances[2].locked, Money::zero(balances[2].currency));
14294
14295 client.refresh_wallet_balances().await.unwrap();
14296 let balances = client.wallet_balance.lock().as_account_balances().unwrap();
14297
14298 assert_eq!(balances.len(), 3);
14299 assert_eq!(client.wallet_balance.lock().token_balances.len(), 2);
14300 assert_eq!(balances[0].total.raw(), 0);
14301 assert_eq!(balances[1].total.raw(), 2_000_000_000_000_000_000);
14302 assert_eq!(balances[2].total.raw(), 12_345_670_000_000_000);
14303 }
14304
14305 #[allow(unsafe_code)] #[tokio::test]
14307 async fn failed_connect_refresh_retains_snapshot_and_publishes_nothing() {
14308 let state = execution_rpc_state()
14309 .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14310 .with_response_sequence(
14311 "eth_call",
14312 &[
14313 CALL_BALANCE_WETH,
14314 CALL_BALANCE_USDC,
14315 CALL_BALANCE_WETH_UPDATED,
14316 RPC_METHOD_NOT_FOUND,
14317 ],
14318 );
14319 let (mut client, _, _) =
14320 client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_ATOMIC").await;
14321 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14322 replace_exec_event_sender(sender);
14323 client.start().unwrap();
14324 client.refresh_wallet_balances().await.unwrap();
14325 let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14326 receiver.try_recv().unwrap();
14327 unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_ATOMIC", TEST_PRIVATE_KEY) };
14329
14330 let error = client.connect().await.unwrap_err();
14331 let failed_address = Address::from_str(USDC).unwrap();
14332
14333 assert!(
14334 error.to_string().contains(&format!(
14335 "failed to fetch token balance for {failed_address}"
14336 )),
14337 "was: {error}"
14338 );
14339 assert!(!client.is_connected());
14340 assert!(client.signer.is_none());
14341 assert_eq!(
14342 client.wallet_balance.lock().as_account_balances().unwrap(),
14343 retained
14344 );
14345 assert!(matches!(
14346 receiver.try_recv(),
14347 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
14348 ));
14349 }
14350
14351 #[allow(unsafe_code)] #[tokio::test]
14353 async fn failed_connect_publication_retains_snapshot() {
14354 let state = execution_rpc_state()
14355 .with_response_sequence("eth_getBalance", &[GET_BALANCE, GET_BALANCE_ZERO])
14356 .with_response_sequence(
14357 "eth_call",
14358 &[
14359 CALL_BALANCE_WETH,
14360 CALL_BALANCE_USDC,
14361 CALL_BALANCE_WETH_UPDATED,
14362 CALL_BALANCE_USDC_UPDATED,
14363 ],
14364 );
14365 let (mut client, _, _) =
14366 client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_BALANCE_PUBLICATION").await;
14367 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14368 replace_exec_event_sender(sender);
14369 client.start().unwrap();
14370 client.refresh_wallet_balances().await.unwrap();
14371 let retained = client.wallet_balance.lock().as_account_balances().unwrap();
14372 receiver.try_recv().unwrap();
14373 drop(receiver);
14374 unsafe { std::env::set_var("BLOCKCHAIN_TEST_BALANCE_PUBLICATION", TEST_PRIVATE_KEY) };
14376
14377 let error = client.connect().await.unwrap_err();
14378
14379 assert!(
14380 error.to_string().contains("Failed to send account state"),
14381 "was: {error}"
14382 );
14383 assert!(!client.is_connected());
14384 assert!(client.signer.is_none());
14385 assert_eq!(
14386 client.wallet_balance.lock().as_account_balances().unwrap(),
14387 retained
14388 );
14389 }
14390
14391 #[allow(unsafe_code)] #[tokio::test]
14393 async fn connect_and_repeated_query_publish_wallet_account_state() {
14394 let state = execution_rpc_state()
14395 .with_response_sequence("eth_call", &[CALL_BALANCE_WETH, CALL_BALANCE_USDC]);
14396 let (mut client, state, cache) =
14397 client_with_token_mock_rpc(state, "BLOCKCHAIN_TEST_ACCOUNT_STATE").await;
14398 let (sender, mut receiver) = tokio::sync::mpsc::unbounded_channel();
14399 replace_exec_event_sender(sender);
14400 client.start().unwrap();
14401 unsafe { std::env::set_var("BLOCKCHAIN_TEST_ACCOUNT_STATE", TEST_PRIVATE_KEY) };
14403
14404 client.connect().await.unwrap();
14405
14406 let ExecutionEvent::Account(connected) = receiver.try_recv().unwrap() else {
14407 panic!("expected account state event")
14408 };
14409 assert_eq!(connected.account_id, AccountId::from("BLOCKCHAIN-001"));
14410 assert_eq!(connected.account_type, AccountType::Wallet);
14411 assert_eq!(connected.base_currency, None);
14412 assert_eq!(connected.balances.len(), 3);
14413 assert!(connected.margins.is_empty());
14414 assert!(connected.is_reported);
14415 cache.borrow_mut().update_account_state(&connected).unwrap();
14416
14417 let account = client.get_account().unwrap();
14418 assert!(matches!(account, AccountAny::Wallet(_)));
14419 assert_eq!(account.id(), AccountId::from("BLOCKCHAIN-001"));
14420 assert_eq!(account.last_event(), Some(connected.clone()));
14421
14422 let requests_before = state.recorded_requests().len();
14423 let query = || {
14424 QueryAccount::new(
14425 TraderId::from("TRADER-001"),
14426 Some(ClientId::from("BLOCKCHAIN-001")),
14427 AccountId::from("BLOCKCHAIN-001"),
14428 UUID4::new(),
14429 UnixNanos::default(),
14430 None,
14431 None,
14432 )
14433 };
14434 client.query_account(query()).unwrap();
14435 client.query_account(query()).unwrap();
14436
14437 let ExecutionEvent::Account(first_query) = receiver.try_recv().unwrap() else {
14438 panic!("expected first query account state event")
14439 };
14440 let ExecutionEvent::Account(second_query) = receiver.try_recv().unwrap() else {
14441 panic!("expected second query account state event")
14442 };
14443 assert!(connected.has_same_balances_and_margins(&first_query));
14444 assert!(first_query.has_same_balances_and_margins(&second_query));
14445 assert_eq!(first_query.account_id, connected.account_id);
14446 assert_eq!(first_query.account_type, connected.account_type);
14447 assert_eq!(first_query.base_currency, connected.base_currency);
14448 assert_eq!(first_query.is_reported, connected.is_reported);
14449 assert_ne!(first_query.event_id, second_query.event_id);
14450 assert_eq!(state.recorded_requests().len(), requests_before);
14451
14452 client.stop().unwrap();
14453 assert!(client.core.is_stopped());
14454 assert!(!client.is_connected());
14455 assert!(client.signer.is_none());
14456 }
14457
14458 #[tokio::test]
14459 async fn connect_rejects_on_chain_mismatch() {
14460 let state = ready_rpc_state().with_response("eth_chainId", CHAIN_ID_ETHEREUM);
14461 let (mut client, _) = client_with_mock_rpc(state).await;
14462
14463 let error = client.connect().await.unwrap_err();
14464
14465 assert!(
14466 error
14467 .to_string()
14468 .contains("Blockchain chain ID verification disagreed"),
14469 "was: {error}"
14470 );
14471 }
14472
14473 #[allow(unsafe_code)] #[tokio::test]
14475 async fn connect_rejects_on_signer_wallet_mismatch() {
14476 let addr = start_mock_rpc_server(ready_rpc_state()).await;
14477 let config = test_config_with_signer_env(
14478 format!("http://{addr}"),
14479 "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14480 );
14481 let mut client = test_client_from_config(config, test_pool());
14482 unsafe {
14485 std::env::set_var(
14486 "BLOCKCHAIN_TEST_PRIVATE_KEY_MISMATCH",
14487 "0x59c6995e998f97a5a0044966f0945389dc9e86dae88c7a8412f4603b6b78690d",
14488 )
14489 };
14490
14491 let error = client.connect().await.unwrap_err();
14492
14493 assert!(
14494 error
14495 .to_string()
14496 .contains("does not match configured wallet address"),
14497 "was: {error}"
14498 );
14499 }
14500
14501 #[allow(unsafe_code)] #[tokio::test]
14503 async fn connect_rejects_missing_trace_capability_before_signer_load() {
14504 let state = ready_rpc_state().with_parameter_response(
14505 "debug_traceTransaction",
14506 &B256::ZERO.to_string(),
14507 RPC_METHOD_NOT_FOUND,
14508 );
14509 let addr = start_mock_rpc_server(state.clone()).await;
14510 let config = test_config_with_signer_env(
14511 format!("http://{addr}"),
14512 "BLOCKCHAIN_TEST_TRACE_CAPABILITY",
14513 );
14514 let mut client = test_client_from_config(config, test_pool());
14515 unsafe { std::env::set_var("BLOCKCHAIN_TEST_TRACE_CAPABILITY", TEST_PRIVATE_KEY) };
14517
14518 let error = client.connect().await.unwrap_err();
14519
14520 assert!(
14521 error
14522 .to_string()
14523 .contains("Blockchain call trace capability verification is locally invalid"),
14524 "was: {error}"
14525 );
14526 assert!(client.signer.is_none());
14527 assert!(
14528 state
14529 .recorded_requests()
14530 .iter()
14531 .all(|request| request["method"] != "eth_getBalance")
14532 );
14533 }
14534
14535 #[allow(unsafe_code)] #[tokio::test]
14537 async fn connect_initializes_signer_from_env() {
14538 let addr = start_mock_rpc_server(ready_rpc_state()).await;
14539 let config =
14540 test_config_with_signer_env(format!("http://{addr}"), "BLOCKCHAIN_TEST_PRIVATE_KEY_OK");
14541 let mut client = test_client_from_config(config, test_pool());
14542 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14543 replace_exec_event_sender(sender);
14544 client.start().unwrap();
14545 unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY_OK", TEST_PRIVATE_KEY) };
14547
14548 client.connect().await.unwrap();
14549
14550 let signer = client.signer.as_ref().unwrap();
14551 assert_eq!(
14552 signer.address(),
14553 address!("f39Fd6e51aad88F6F4ce6aB8827279cffFb92266")
14554 );
14555 }
14556
14557 #[allow(unsafe_code)] #[tokio::test]
14559 async fn reconnect_resumes_at_the_durable_finalized_tip() {
14560 let Some((admin_pool, pg_config)) =
14561 connect_test_postgres("verification ledger resume").await
14562 else {
14563 return;
14564 };
14565 let schema = format!("verification_ledger_resume_{}", std::process::id());
14566 setup_execution_schema(&admin_pool, &schema).await;
14567 let options: sqlx::postgres::PgConnectOptions = pg_config.into();
14568 let options = options.options([("search_path", schema.clone())]);
14569 let database = connect_test_database(options).await.unwrap();
14570 database
14571 .ensure_execution_transaction_schema()
14572 .await
14573 .unwrap();
14574
14575 let state = ready_rpc_state();
14576 let addr = start_mock_rpc_server(state.clone()).await;
14577 let config = test_config_with_signer_env(
14578 format!("http://{addr}"),
14579 "BLOCKCHAIN_TEST_VERIFICATION_RESUME",
14580 );
14581 let mut client = test_client_from_config(config, test_pool());
14582 client.cache.database = Some(database);
14583 protect_test_storage(&mut client, &schema).await;
14584 let finalized_headers = [
14585 ExecutionVerifiedHeader {
14586 number: FIXTURE_BLOCK,
14587 hash: FIXTURE_BLOCK_HASH.to_string(),
14588 parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000001"
14589 .to_string(),
14590 timestamp: FIXTURE_BLOCK_TIMESTAMP,
14591 base_fee_per_gas: Some(100_000_000),
14592 },
14593 ExecutionVerifiedHeader {
14594 number: FIXTURE_BLOCK + 1,
14595 hash: B256::from([0x22; 32]).to_string(),
14596 parent_hash: FIXTURE_BLOCK_HASH.to_string(),
14597 timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
14598 base_fee_per_gas: Some(100_000_000),
14599 },
14600 ExecutionVerifiedHeader {
14601 number: FIXTURE_BLOCK + 2,
14602 hash: B256::from([0x33; 32]).to_string(),
14603 parent_hash: B256::from([0x22; 32]).to_string(),
14604 timestamp: FIXTURE_BLOCK_TIMESTAMP + 2,
14605 base_fee_per_gas: Some(100_000_000),
14606 },
14607 ];
14608 initialize_test_verification_ledger_with_headers(&client, &finalized_headers).await;
14609 let verification = client.config.verification.as_ref().unwrap();
14610 let position = client
14611 .cache
14612 .database
14613 .as_ref()
14614 .unwrap()
14615 .load_execution_verification_position(
14616 42_161,
14617 WALLET,
14618 &verification.manifest_version,
14619 &verification.manifest_digest,
14620 )
14621 .await
14622 .unwrap()
14623 .unwrap();
14624 let resume = client
14625 .cache
14626 .database
14627 .as_ref()
14628 .unwrap()
14629 .load_execution_verification_resume(
14630 42_161,
14631 WALLET,
14632 &verification.manifest_version,
14633 &verification.manifest_digest,
14634 )
14635 .await
14636 .unwrap()
14637 .unwrap();
14638 assert_eq!(position.next_canonical_nonce, 7);
14639 assert_eq!(position.revision, 0);
14640 assert_eq!(position.finalized_tip, finalized_headers[2]);
14641 assert_eq!(resume.next_canonical_nonce, 7);
14642 assert_eq!(resume.revision, 0);
14643 assert_eq!(resume.finalized_headers, finalized_headers);
14644
14645 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14646 replace_exec_event_sender(sender);
14647 client.start().unwrap();
14648 unsafe { std::env::set_var("BLOCKCHAIN_TEST_VERIFICATION_RESUME", TEST_PRIVATE_KEY) };
14650
14651 client.connect().await.unwrap();
14652
14653 let skipped_height = format!("0x{:x}", FIXTURE_BLOCK + 1);
14654 assert!(client.is_connected());
14655 assert!(state.recorded_requests().iter().all(|request| {
14656 request["method"] != "eth_getBlockByNumber"
14657 || request["params"][0].as_str() != Some(&skipped_height)
14658 }));
14659 let header_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14660 "SELECT COUNT(*) FROM {schema}.execution_verified_finalized_header"
14661 )))
14662 .fetch_one(&admin_pool)
14663 .await
14664 .unwrap();
14665 assert_eq!(header_count, 3);
14666
14667 drop(client);
14668 drop_execution_schema(&admin_pool, &schema).await;
14669 }
14670
14671 #[tokio::test]
14672 async fn disconnect_revokes_signer_and_blocks_execution() {
14673 let (mut client, state) = client_with_mock_rpc(ready_rpc_state()).await;
14674 client.core.set_connected();
14675 client.signer = Some(Arc::new(
14676 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14677 ));
14678
14679 client.disconnect().await.unwrap();
14680 let error = client.wrap(U256::from(1_000u64)).await.unwrap_err();
14681
14682 assert!(!client.is_connected());
14683 assert!(client.signer.is_none());
14684 assert!(
14685 error.to_string().contains("is not connected"),
14686 "was: {error}"
14687 );
14688 assert!(state.recorded_requests().is_empty());
14689 }
14690
14691 #[allow(unsafe_code)] #[tokio::test]
14693 async fn connect_releases_stale_preparing_claim_after_tasks_finish() {
14694 let addr = start_mock_rpc_server(ready_rpc_state()).await;
14695 let config = test_config_with_signer_env(
14696 format!("http://{addr}"),
14697 "BLOCKCHAIN_TEST_RECONNECT_CLAIM",
14698 );
14699 let mut client = test_client_from_config(config, test_pool());
14700 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14701 replace_exec_event_sender(sender);
14702 client.start().unwrap();
14703 unsafe { std::env::set_var("BLOCKCHAIN_TEST_RECONNECT_CLAIM", TEST_PRIVATE_KEY) };
14705 *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Swap));
14706 client
14707 .pending_tasks
14708 .spawn(async {})
14709 .expect("stale task spawn");
14710
14711 client.connect().await.unwrap();
14712
14713 assert!(client.in_flight.lock().is_none());
14714 }
14715
14716 #[allow(unsafe_code)] #[tokio::test]
14718 async fn reconnect_after_aborted_submission_releases_preparing_claim() {
14719 let state = ready_rpc_state().with_sleep("eth_getBlockByNumber", Duration::from_secs(30));
14722 let Some((admin_pool, schema, mut client, state, _)) =
14723 swap_client_with_database("execution_reconnect_claim_test", state).await
14724 else {
14725 return;
14726 };
14727 let (sender, _receiver) = tokio::sync::mpsc::unbounded_channel();
14728 replace_exec_event_sender(sender);
14729 client.start().unwrap();
14730 unsafe { std::env::set_var("BLOCKCHAIN_TEST_PRIVATE_KEY", TEST_PRIVATE_KEY) };
14732
14733 let order = test_market_sell_order(test_pool().instrument_id);
14734 client.submit_order(submit_order_cmd(&order)).unwrap();
14735 await_recorded_requests(&state, "eth_getBlockByNumber", 1).await;
14736 assert!(client.in_flight.lock().is_some());
14737
14738 client.disconnect().await.unwrap();
14739 let ready_addr = start_mock_rpc_server(ready_rpc_state()).await;
14740 let ready = test_client(format!("http://{ready_addr}"));
14741 client.http_rpc_client = ready.http_rpc_client;
14742 client.verification = ready.verification;
14743 client.connect().await.unwrap();
14744
14745 assert!(client.in_flight.lock().is_none());
14746
14747 drop_execution_schema(&admin_pool, &schema).await;
14748 }
14749
14750 #[tokio::test]
14751 async fn poll_for_receipt_returns_none_after_exhaustion() {
14752 let state = ready_rpc_state().with_response("eth_getTransactionReceipt", RECEIPT_NULL);
14753 let (client, state) = client_with_mock_rpc(state).await;
14754
14755 let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14756 .await
14757 .unwrap();
14758
14759 assert!(receipt.is_none());
14760 let requests = state.recorded_requests();
14761 assert_eq!(requests.len(), 3);
14762 }
14763
14764 #[tokio::test]
14765 async fn poll_for_receipt_returns_last_error_when_every_poll_fails() {
14766 let state =
14767 ready_rpc_state().with_response("eth_getTransactionReceipt", RPC_METHOD_NOT_FOUND);
14768 let (client, state) = client_with_mock_rpc(state).await;
14769
14770 let error = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14771 .await
14772 .unwrap_err();
14773
14774 assert_eq!(
14775 error.to_string(),
14776 "eth_getTransactionReceipt RPC error -32601"
14777 );
14778 let requests = state.recorded_requests();
14779 assert_eq!(requests.len(), 3);
14780 }
14781
14782 #[tokio::test]
14783 async fn poll_for_receipt_returns_none_after_pending_then_errors() {
14784 let state = ready_rpc_state().with_response_sequence(
14785 "eth_getTransactionReceipt",
14786 &[RECEIPT_NULL, RPC_METHOD_NOT_FOUND, RPC_METHOD_NOT_FOUND],
14787 );
14788 let (client, state) = client_with_mock_rpc(state).await;
14789
14790 let receipt = poll_for_receipt(&client.http_rpc_client, &B256::ZERO, 3, Duration::ZERO)
14791 .await
14792 .unwrap();
14793
14794 assert!(receipt.is_none());
14795 let requests = state.recorded_requests();
14796 assert_eq!(requests.len(), 3);
14797 }
14798
14799 #[tokio::test]
14800 async fn cancellation_during_persistence_keeps_in_flight_slot() {
14801 let Some((admin_pool, pg_config)) = connect_test_postgres("persistence cancellation").await
14802 else {
14803 return;
14804 };
14805
14806 let schema = format!("execution_persist_cancel_test_{}", std::process::id());
14807 setup_execution_schema(&admin_pool, &schema).await;
14808
14809 let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
14810 let db_options = db_options.options([("search_path", schema.clone())]);
14811 let database = connect_test_database(db_options).await.unwrap();
14812
14813 let state = signing_rpc_state();
14814 let addr = start_mock_rpc_server(state.clone()).await;
14815
14816 let mut client = test_client(format!("http://{addr}"));
14817 client.cache.database = Some(database);
14818 client
14820 .cache
14821 .ensure_execution_transaction_schema()
14822 .await
14823 .unwrap();
14824 protect_test_storage(&mut client, &schema).await;
14825 initialize_test_verification_ledger(&client).await;
14826 client.signer = Some(Arc::new(
14827 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
14828 ));
14829 client.core.set_connected();
14830
14831 let advisory_lock = i64::from(std::process::id());
14832
14833 for statement in [
14834 format!(
14835 "CREATE FUNCTION {schema}.block_execution_hash_insert() RETURNS trigger \
14836 LANGUAGE plpgsql AS 'BEGIN PERFORM pg_advisory_xact_lock({advisory_lock}); \
14837 RETURN NEW; END'"
14838 ),
14839 format!(
14840 "CREATE TRIGGER block_execution_hash_insert BEFORE INSERT ON \
14841 {schema}.execution_transaction_hash FOR EACH ROW EXECUTE FUNCTION \
14842 {schema}.block_execution_hash_insert()"
14843 ),
14844 ] {
14845 sqlx::query(sqlx::AssertSqlSafe(statement))
14846 .execute(&admin_pool)
14847 .await
14848 .unwrap();
14849 }
14850 let mut lock_transaction = admin_pool.begin().await.unwrap();
14851 sqlx::query("SELECT pg_advisory_xact_lock($1)")
14852 .bind(advisory_lock)
14853 .execute(&mut *lock_transaction)
14854 .await
14855 .unwrap();
14856
14857 let value = U256::from(1_000_000_000_000_000u64);
14858 let in_flight = Arc::clone(&client.in_flight);
14859 let mut wrap = Box::pin(client.wrap(value));
14860 tokio::time::timeout(Duration::from_secs(2), async {
14861 loop {
14862 tokio::select! {
14863 result = &mut wrap => {
14864 panic!("persistence completed while the table was locked: {result:?}")
14865 }
14866 () = tokio::time::sleep(Duration::from_millis(1)) => {}
14867 }
14868
14869 if matches!(*in_flight.lock(), Some(InFlightSlot::AwaitingFinality(_))) {
14870 break;
14871 }
14872 }
14873 })
14874 .await
14875 .unwrap();
14876 drop(wrap);
14877 lock_transaction.rollback().await.unwrap();
14878
14879 let slot = *client.in_flight.lock();
14880 let second_error = client
14881 .wrap(U256::from(2_000_000_000_000_000u64))
14882 .await
14883 .unwrap_err();
14884 let broadcasts = state
14885 .recorded_requests()
14886 .into_iter()
14887 .filter(|request| request["method"] == "eth_sendRawTransaction")
14888 .count();
14889 let status: String = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14890 "SELECT status FROM {schema}.execution_intent"
14891 )))
14892 .fetch_one(&admin_pool)
14893 .await
14894 .unwrap();
14895
14896 assert!(matches!(slot, Some(InFlightSlot::AwaitingFinality(_))));
14897 assert_eq!(status, "prepared");
14898 assert!(
14899 second_error.to_string().contains("awaiting finality"),
14900 "was: {second_error}"
14901 );
14902 assert_eq!(broadcasts, 0);
14903
14904 drop_execution_schema(&admin_pool, &schema).await;
14905 }
14906
14907 #[tokio::test]
14908 async fn reservation_failure_releases_preparing_slot() {
14909 let state = signing_rpc_state();
14910 let Some((admin_pool, schema, mut client, state)) =
14911 execution_client_with_database("execution_reservation_fail_test", state).await
14912 else {
14913 return;
14914 };
14915 sqlx::query(sqlx::AssertSqlSafe(format!(
14916 "DROP TABLE {schema}.execution_intent CASCADE"
14917 )))
14918 .execute(&admin_pool)
14919 .await
14920 .unwrap();
14921
14922 let error = client
14923 .wrap(U256::from(1_000_000_000_000_000u64))
14924 .await
14925 .unwrap_err();
14926 let retry_error = client
14927 .wrap(U256::from(2_000_000_000_000_000u64))
14928 .await
14929 .unwrap_err();
14930 let broadcasts = state
14931 .recorded_requests()
14932 .into_iter()
14933 .filter(|request| request["method"] == "eth_sendRawTransaction")
14934 .count();
14935
14936 assert_eq!(
14937 error.to_string(),
14938 "Execution intent reservation failed before commit"
14939 );
14940 assert!(reservation_failure_proven_not_committed(&error));
14941 assert_eq!(
14942 retry_error.to_string(),
14943 "Execution intent reservation failed before commit"
14944 );
14945 assert_eq!(broadcasts, 0);
14946 assert!(client.in_flight.lock().is_none());
14947
14948 drop_execution_schema(&admin_pool, &schema).await;
14949 }
14950
14951 #[tokio::test]
14952 async fn reservation_commit_failure_keeps_preparing_slot() {
14953 let state = signing_rpc_state();
14954 let Some((admin_pool, schema, mut client, state)) =
14955 execution_client_with_database("execution_reservation_commit_fail_test", state).await
14956 else {
14957 return;
14958 };
14959 install_reservation_commit_rejection(&admin_pool, &schema).await;
14960
14961 let error = client
14962 .wrap(U256::from(1_000_000_000_000_000u64))
14963 .await
14964 .unwrap_err();
14965 let slot = *client.in_flight.lock();
14966 let second_error = client
14967 .wrap(U256::from(2_000_000_000_000_000u64))
14968 .await
14969 .unwrap_err();
14970 let requests = state.recorded_requests();
14971 let intent_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14972 "SELECT COUNT(*) FROM {schema}.execution_intent"
14973 )))
14974 .fetch_one(&admin_pool)
14975 .await
14976 .unwrap();
14977 let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
14978 "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
14979 )))
14980 .fetch_one(&admin_pool)
14981 .await
14982 .unwrap();
14983
14984 assert_eq!(
14985 error.to_string(),
14986 "Execution intent reservation commit outcome is unknown; reconciliation is required"
14987 );
14988 assert!(!reservation_failure_proven_not_committed(&error));
14989 assert!(matches!(
14990 slot,
14991 Some(InFlightSlot::Preparing(TransactionPurpose::Wrap))
14992 ));
14993 assert!(
14994 second_error.to_string().contains("being prepared"),
14995 "was: {second_error}"
14996 );
14997 assert_eq!(intent_count, 0);
14998 assert_eq!(signed_count, 0);
14999 assert!(
15000 requests
15001 .iter()
15002 .all(|request| request["method"] != "eth_getTransactionCount")
15003 );
15004 assert!(
15005 requests
15006 .iter()
15007 .all(|request| request["method"] != "eth_sendRawTransaction")
15008 );
15009
15010 drop_execution_schema(&admin_pool, &schema).await;
15011 }
15012
15013 #[tokio::test]
15014 async fn persistence_failure_keeps_unbroadcast_slot() {
15015 let state = signing_rpc_state();
15016 let Some((admin_pool, schema, mut client, state)) =
15017 execution_client_with_database("execution_persist_fail_test", state).await
15018 else {
15019 return;
15020 };
15021 sqlx::query(sqlx::AssertSqlSafe(format!(
15022 "ALTER TABLE {schema}.execution_transaction_hash \
15023 ADD CONSTRAINT execution_hash_reject CHECK (FALSE)"
15024 )))
15025 .execute(&admin_pool)
15026 .await
15027 .unwrap();
15028
15029 let value = U256::from(1_000_000_000_000_000u64);
15030 let error = client.wrap(value).await.unwrap_err();
15031 let in_flight = awaiting_in_flight(&client);
15032 let second_error = client
15033 .wrap(U256::from(2_000_000_000_000_000u64))
15034 .await
15035 .unwrap_err();
15036 let broadcasts = state
15037 .recorded_requests()
15038 .into_iter()
15039 .filter(|request| request["method"] == "eth_sendRawTransaction")
15040 .count();
15041 let expected_hash = expected_wrap_tx_hash(value).await;
15042
15043 let error_message = error.to_string();
15044 assert!(error_message.starts_with(&format!(
15045 "Failed to persist transaction {expected_hash}: Failed to persist signed transaction"
15046 )), "was: {error_message}");
15047 assert!(
15048 error_message.ends_with("the in-flight slot stays occupied"),
15049 "was: {error_message}"
15050 );
15051 assert_eq!(in_flight.nonce, 7);
15052 assert_eq!(in_flight.tx_hash, expected_hash);
15053 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15054 assert!(
15055 second_error.to_string().contains("still awaiting finality"),
15056 "was: {second_error}"
15057 );
15058 assert_eq!(broadcasts, 0);
15059
15060 drop_execution_schema(&admin_pool, &schema).await;
15061 }
15062
15063 #[tokio::test]
15064 async fn cancellation_after_dispatch_keeps_record_and_in_flight_slot() {
15065 let state = signing_rpc_state().with_sleep(
15066 "eth_sendRawTransaction",
15067 Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15068 );
15069 let Some((admin_pool, schema, mut client, state)) =
15070 execution_client_with_database("execution_cancel_test", state).await
15071 else {
15072 return;
15073 };
15074
15075 let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15076 tokio::select! {
15077 result = &mut wrap => panic!("broadcast completed before cancellation: {result:?}"),
15078 () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15079 }
15080 drop(wrap);
15081
15082 let in_flight = awaiting_in_flight(&client);
15083 let error = client
15084 .wrap(U256::from(2_000_000_000_000_000u64))
15085 .await
15086 .unwrap_err();
15087 let record = client
15088 .cache
15089 .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15090 .await
15091 .unwrap()
15092 .unwrap();
15093 let broadcasts = state
15094 .recorded_requests()
15095 .into_iter()
15096 .filter(|request| request["method"] == "eth_sendRawTransaction")
15097 .count();
15098
15099 assert!(
15100 error.to_string().contains("still awaiting finality"),
15101 "was: {error}"
15102 );
15103 assert_eq!(record.nonce, 7);
15104 assert_eq!(record.purpose, "wrap");
15105 assert_eq!(record.status, "broadcast");
15106 assert_eq!(broadcasts, 1);
15107
15108 drop_execution_schema(&admin_pool, &schema).await;
15109 }
15110
15111 #[tokio::test]
15112 async fn cancellation_during_receipt_polling_keeps_record_and_in_flight_slot() {
15113 let state = signing_rpc_state()
15114 .with_send_raw_transaction_echo()
15115 .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
15116 .with_sleep(
15117 "eth_getTransactionReceipt",
15118 Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15119 );
15120 let Some((admin_pool, schema, mut client, state)) =
15121 execution_client_with_database("execution_receipt_cancel_test", state).await
15122 else {
15123 return;
15124 };
15125
15126 let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15127 tokio::select! {
15128 result = &mut wrap => panic!("receipt polling completed before cancellation: {result:?}"),
15129 () = await_recorded_requests(&state, "eth_getTransactionReceipt", 3) => {}
15130 }
15131 drop(wrap);
15132
15133 let in_flight = awaiting_in_flight(&client);
15134 let second_error = client
15135 .wrap(U256::from(2_000_000_000_000_000u64))
15136 .await
15137 .unwrap_err();
15138 let record = client
15139 .cache
15140 .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15141 .await
15142 .unwrap()
15143 .unwrap();
15144 let requests = state.recorded_requests();
15145
15146 assert_eq!(in_flight.nonce, 7);
15147 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15148 assert!(
15149 second_error.to_string().contains("still awaiting finality"),
15150 "was: {second_error}"
15151 );
15152 assert_eq!(record.nonce, 7);
15153 assert_eq!(record.transaction_hash, in_flight.tx_hash.to_string());
15154 assert_eq!(record.purpose, "wrap");
15155 assert_eq!(record.status, "broadcast");
15156 assert_eq!(
15157 requests
15158 .iter()
15159 .filter(|request| request["method"] == "eth_sendRawTransaction")
15160 .count(),
15161 1
15162 );
15163 assert_eq!(
15164 requests
15165 .iter()
15166 .filter(|request| request["method"] == "eth_getTransactionReceipt")
15167 .count(),
15168 3
15169 );
15170
15171 drop_execution_schema(&admin_pool, &schema).await;
15172 }
15173
15174 #[tokio::test]
15175 async fn rejected_broadcast_stays_dropped_and_occupied() {
15176 let state = signing_rpc_state()
15177 .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15178 .with_response("eth_getTransactionReceipt", RECEIPT_NULL);
15179 let Some((admin_pool, schema, mut client, state)) =
15180 execution_client_with_database("execution_rejected_test", state).await
15181 else {
15182 return;
15183 };
15184
15185 let error = client
15186 .wrap(U256::from(1_000_000_000_000_000u64))
15187 .await
15188 .unwrap_err();
15189 let (purpose, status): (String, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15190 "SELECT purpose, status FROM {schema}.execution_intent"
15191 )))
15192 .fetch_one(&admin_pool)
15193 .await
15194 .unwrap();
15195 let broadcasts = state
15196 .recorded_requests()
15197 .into_iter()
15198 .filter(|request| request["method"] == "eth_sendRawTransaction")
15199 .count();
15200
15201 assert!(
15202 error.to_string().contains("Timed out awaiting finality"),
15203 "was: {error}"
15204 );
15205 assert!(client.in_flight.lock().is_some());
15206 assert_eq!(purpose, "wrap");
15207 assert_eq!(status, "dropped");
15208 assert_eq!(broadcasts, 1);
15209
15210 drop_execution_schema(&admin_pool, &schema).await;
15211 }
15212
15213 #[tokio::test]
15214 async fn legacy_table_loss_does_not_release_rejected_broadcast() {
15215 let state = signing_rpc_state()
15216 .with_response("eth_sendRawTransaction", SEND_RAW_TRANSACTION_REJECTED)
15217 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15218 .with_sleep("eth_sendRawTransaction", Duration::from_secs(1));
15219 let Some((admin_pool, schema, mut client, state)) =
15220 execution_client_with_database("execution_rejected_update_test", state).await
15221 else {
15222 return;
15223 };
15224
15225 let mut wrap = Box::pin(client.wrap(U256::from(1_000_000_000_000_000u64)));
15226 tokio::select! {
15227 result = &mut wrap => panic!("broadcast completed before database failure: {result:?}"),
15228 () = await_recorded_requests(&state, "eth_sendRawTransaction", 1) => {}
15229 }
15230 sqlx::query(sqlx::AssertSqlSafe(format!(
15231 "DROP TABLE {schema}.execution_transaction"
15232 )))
15233 .execute(&admin_pool)
15234 .await
15235 .unwrap();
15236
15237 let error = wrap.await.unwrap_err();
15238
15239 assert!(
15240 error.to_string().contains("Timed out awaiting finality"),
15241 "was: {error}"
15242 );
15243 let in_flight = awaiting_in_flight(&client);
15244 assert_eq!(in_flight.nonce, 7);
15245 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15246
15247 drop_execution_schema(&admin_pool, &schema).await;
15248 }
15249
15250 #[tokio::test]
15251 async fn finalized_receipt_keeps_slot_when_status_update_fails() {
15252 let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
15253 let block = finalized_wrap_block(expected_hash);
15254 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15255 let state = with_finalized_identity(
15256 signing_rpc_state()
15257 .with_send_raw_transaction_echo()
15258 .with_response("eth_getTransactionReceipt", &receipt)
15259 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15260 .with_call_response_sequence(
15261 BALANCE_OF_SELECTOR,
15262 &[
15263 CALL_BALANCE,
15264 CALL_BALANCE,
15265 CALL_BALANCE,
15266 CALL_BALANCE,
15267 CALL_BALANCE,
15268 CALL_BALANCE,
15269 CALL_BALANCE_AFTER_WRAP,
15270 CALL_BALANCE_AFTER_WRAP,
15271 CALL_BALANCE_AFTER_WRAP,
15272 ],
15273 ),
15274 &block,
15275 &receipt,
15276 );
15277 let Some((admin_pool, schema, mut client, state)) =
15278 execution_client_with_database("execution_finalized_update_test", state).await
15279 else {
15280 return;
15281 };
15282
15283 for statement in [
15284 format!(
15285 "CREATE FUNCTION {schema}.reject_finalized_status() RETURNS trigger \
15286 LANGUAGE plpgsql AS 'BEGIN IF NEW.status = ''finalized'' THEN \
15287 RAISE EXCEPTION ''test finalized status rejection''; END IF; \
15288 RETURN NEW; END'"
15289 ),
15290 format!(
15291 "CREATE TRIGGER reject_finalized_status BEFORE UPDATE ON \
15292 {schema}.execution_transaction_hash FOR EACH ROW \
15293 EXECUTE FUNCTION {schema}.reject_finalized_status()"
15294 ),
15295 ] {
15296 sqlx::query(sqlx::AssertSqlSafe(statement))
15297 .execute(&admin_pool)
15298 .await
15299 .unwrap();
15300 }
15301
15302 let error = client
15303 .wrap(U256::from(1_000_000_000_000_000u64))
15304 .await
15305 .unwrap_err();
15306 let in_flight = awaiting_in_flight(&client);
15307 let requests = state.recorded_requests();
15308
15309 assert!(
15310 error
15311 .to_string()
15312 .contains("test finalized status rejection"),
15313 "was: {error}"
15314 );
15315 assert_eq!(in_flight.nonce, 7);
15316 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15317 assert_eq!(
15318 requests
15319 .iter()
15320 .filter(|request| request["method"] == "eth_sendRawTransaction")
15321 .count(),
15322 1
15323 );
15324 assert_eq!(
15325 requests
15326 .iter()
15327 .filter(|request| request["method"] == "eth_getTransactionReceipt")
15328 .count(),
15329 3
15330 );
15331
15332 drop_execution_schema(&admin_pool, &schema).await;
15333 }
15334
15335 #[tokio::test]
15336 async fn broadcast_timeout_marks_dropped_and_keeps_ownership() {
15337 let state = signing_rpc_state()
15338 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15339 .with_sleep(
15340 "eth_sendRawTransaction",
15341 Duration::from_secs(EXECUTION_RPC_TIMEOUT_SECS + 2),
15342 );
15343 let Some((admin_pool, schema, mut client, _)) =
15344 execution_client_with_database("execution_timeout_test", state).await
15345 else {
15346 return;
15347 };
15348
15349 let error = client
15350 .wrap(U256::from(1_000_000_000_000_000u64))
15351 .await
15352 .unwrap_err();
15353
15354 assert!(
15355 error.to_string().contains("Timed out awaiting finality"),
15356 "was: {error}"
15357 );
15358 let in_flight = awaiting_in_flight(&client);
15359 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
15360 assert_eq!(in_flight.nonce, 7);
15361
15362 let record = client
15364 .cache
15365 .get_execution_transaction(42161, &in_flight.tx_hash.to_string())
15366 .await
15367 .unwrap()
15368 .unwrap();
15369 assert_eq!(record.status, "dropped");
15370
15371 drop_execution_schema(&admin_pool, &schema).await;
15372 }
15373
15374 #[tokio::test]
15375 async fn restart_reconciles_dropped_transaction_without_rebroadcast() {
15376 let initial_state = broadcast_rpc_state()
15377 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15378 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15379 let Some((admin_pool, schema, mut first_client, _)) =
15380 execution_client_with_database("execution_restart_test", initial_state).await
15381 else {
15382 return;
15383 };
15384 let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
15385 let error = first_client
15386 .wrap(U256::from(1_000_000_000_000_000_u64))
15387 .await
15388 .unwrap_err();
15389 assert!(
15390 error.to_string().contains("Timed out awaiting finality"),
15391 "was: {error}"
15392 );
15393 let database = first_client.cache.database.as_ref().unwrap().clone();
15394 let payload_keys = first_client.payload_keys.clone();
15395 drop(first_client);
15396
15397 let block = finalized_wrap_block(expected_hash);
15398 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15399 let restart_state = with_finalized_identity(
15400 execution_rpc_state()
15401 .with_response("eth_getTransactionReceipt", &receipt)
15402 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15403 .with_response_sequence(
15404 "eth_call",
15405 &[
15406 CALL_BALANCE,
15407 CALL_BALANCE,
15408 CALL_BALANCE,
15409 CALL_BALANCE_AFTER_WRAP,
15410 CALL_BALANCE_AFTER_WRAP,
15411 CALL_BALANCE_AFTER_WRAP,
15412 ],
15413 ),
15414 &block,
15415 &receipt,
15416 );
15417 let addr = start_mock_rpc_server(restart_state.clone()).await;
15418 let mut restarted = test_client(format!("http://{addr}"));
15419 restarted.cache.database = Some(database);
15420 restarted.payload_keys = payload_keys;
15421 restarted.signer = Some(Arc::new(
15422 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
15423 ));
15424
15425 restarted.reconcile_unresolved_execution().await.unwrap();
15426 restarted.reconcile_unresolved_execution().await.unwrap();
15427
15428 let record = restarted
15429 .cache
15430 .get_execution_transaction(42161, &expected_hash.to_string())
15431 .await
15432 .unwrap()
15433 .unwrap();
15434 let requests = restart_state.recorded_requests();
15435 assert_eq!(record.status, "finalized");
15436 assert!(restarted.in_flight.lock().is_none());
15437 assert_eq!(
15438 requests
15439 .iter()
15440 .filter(|request| request["method"] == "eth_getTransactionReceipt")
15441 .count(),
15442 6
15443 );
15444 assert_eq!(
15445 requests
15446 .iter()
15447 .filter(|request| {
15448 request["method"] == "eth_call"
15449 && request["params"][0]["data"]
15450 .as_str()
15451 .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
15452 })
15453 .count(),
15454 6
15455 );
15456 assert_eq!(
15457 requests
15458 .iter()
15459 .filter(|request| request["method"] == "eth_sendRawTransaction")
15460 .count(),
15461 0
15462 );
15463
15464 drop_execution_schema(&admin_pool, &schema).await;
15465 }
15466
15467 fn migration_test_intent(id: i64) -> ExecutionIntentRow {
15468 ExecutionIntentRow {
15469 id,
15470 schema_version: crate::execution::transaction::EXECUTION_SCHEMA_VERSION,
15471 chain_id: 42_161,
15472 wallet_address: WALLET.to_string(),
15473 nonce: None,
15474 purpose: "wrap".to_string(),
15475 status: "prepared".to_string(),
15476 client_order_id: None,
15477 trader_id: None,
15478 strategy_id: None,
15479 account_id: None,
15480 instrument_id: None,
15481 pool_address: None,
15482 transaction_to: WETH.to_string(),
15483 transaction_input: "0xd0e30db0".to_string(),
15484 transaction_value: "1".to_string(),
15485 amount_in: None,
15486 created_block: FIXTURE_BLOCK,
15487 acknowledgement_emitted: false,
15488 fill_emitted: false,
15489 terminal_emitted: false,
15490 active: true,
15491 }
15492 }
15493
15494 fn migration_nonce_verification() -> Verified<u64> {
15495 Verified {
15496 value: 7,
15497 read: crate::rpc::verification::VerificationRead::TransactionCount,
15498 provider_ids: [
15499 "authoritative".to_string(),
15500 "verifier-a".to_string(),
15501 "verifier-b".to_string(),
15502 ],
15503 normalized_value_digest: keccak256(7_u64.to_be_bytes()),
15504 }
15505 }
15506
15507 fn migration_finalized_header() -> VerifiedBlockHeader {
15508 VerifiedBlockHeader {
15509 number: FIXTURE_BLOCK,
15510 hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15511 parent_hash: B256::from_str(
15512 "0x0000000000000000000000000000000000000000000000000000000000000001",
15513 )
15514 .unwrap(),
15515 timestamp: FIXTURE_BLOCK_TIMESTAMP,
15516 base_fee_per_gas: Some(100_000_000),
15517 }
15518 }
15519
15520 #[tokio::test]
15521 async fn verification_migration_recovers_prepared_unassigned_intent() {
15522 let client = test_client("http://127.0.0.1:1".to_string());
15523 let snapshot = ExecutionVerificationMigrationSnapshot {
15524 intents: vec![migration_test_intent(1)],
15525 hashes: Vec::new(),
15526 };
15527 let finalized = migration_finalized_header();
15528 let migration = client
15529 .build_execution_verification_migration(
15530 snapshot,
15531 finalized,
15532 &[finalized],
15533 &migration_nonce_verification(),
15534 )
15535 .await
15536 .unwrap();
15537
15538 assert_eq!(migration.records.len(), 1);
15539 let record = &migration.records[0];
15540 assert_eq!(record.intent_id, 1);
15541 assert_eq!(record.nonce, None);
15542 assert_eq!(record.transaction_hash, None);
15543 assert!(record.recover_prepared);
15544 assert_eq!(record.decisions.len(), 1);
15545 }
15546
15547 #[tokio::test]
15548 async fn verification_migration_rejects_inconsistent_released_history() {
15549 let client = test_client("http://127.0.0.1:1".to_string());
15550 let finalized = migration_finalized_header();
15551 let nonce_verification = migration_nonce_verification();
15552 let mut missing_marker = migration_test_intent(1);
15553 missing_marker.active = false;
15554 missing_marker.status = "finalized".to_string();
15555 missing_marker.nonce = Some(6);
15556 let error = client
15557 .build_execution_verification_migration(
15558 ExecutionVerificationMigrationSnapshot {
15559 intents: vec![missing_marker],
15560 hashes: Vec::new(),
15561 },
15562 finalized,
15563 &[finalized],
15564 &nonce_verification,
15565 )
15566 .await
15567 .err()
15568 .unwrap();
15569 assert!(
15570 error.to_string().contains("has no durable event marker"),
15571 "was: {error}"
15572 );
15573
15574 let mut first = migration_test_intent(1);
15575 first.active = false;
15576 first.status = "recoverable".to_string();
15577 first.nonce = Some(6);
15578 let mut second = first.clone();
15579 second.id = 2;
15580 let error = client
15581 .build_execution_verification_migration(
15582 ExecutionVerificationMigrationSnapshot {
15583 intents: vec![first, second],
15584 hashes: Vec::new(),
15585 },
15586 finalized,
15587 &[finalized],
15588 &nonce_verification,
15589 )
15590 .await
15591 .err()
15592 .unwrap();
15593 assert!(
15594 error
15595 .to_string()
15596 .contains("duplicate signer nonce ownership"),
15597 "was: {error}"
15598 );
15599 }
15600
15601 #[tokio::test]
15602 async fn verification_migration_reconstructs_consumed_active_intent() {
15603 let expected_hash = expected_wrap_tx_hash(U256::from(1u64)).await;
15604 let mut block_value: serde_json::Value =
15605 serde_json::from_str(&finalized_wrap_block(expected_hash)).unwrap();
15606 block_value["result"]["transactions"][0]["value"] = serde_json::json!("0x1");
15607 let block = block_value.to_string();
15608 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15609 let state = with_finalized_identity(
15610 execution_rpc_state()
15611 .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
15612 .with_response("eth_getTransactionReceipt", &receipt)
15613 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
15614 &block,
15615 &receipt,
15616 );
15617 let Some((admin_pool, pg_config)) =
15618 connect_test_postgres("verification_migration_reconstructs_consumed_active_intent")
15619 .await
15620 else {
15621 return;
15622 };
15623 let schema = format!(
15624 "verification_migration_reconstructs_consumed_active_intent_{}",
15625 std::process::id()
15626 );
15627 setup_execution_schema(&admin_pool, &schema).await;
15628 let options: sqlx::postgres::PgConnectOptions = pg_config.into();
15629 let database = connect_test_database(options.options([("search_path", schema.clone())]))
15630 .await
15631 .unwrap();
15632 let addr = start_mock_rpc_server(state).await;
15633 let (mut client, _) = swap_client_with_cache(test_config(format!("http://{addr}")));
15634 client.cache.database = Some(database.clone());
15635 client
15636 .cache
15637 .ensure_execution_transaction_schema()
15638 .await
15639 .unwrap();
15640 let (intent, persisted_hash, _) = persist_test_wrap_broadcast(&database, None).await;
15641 protect_test_storage(&mut client, &schema).await;
15642 assert_eq!(persisted_hash, expected_hash);
15643
15644 let snapshot = database
15645 .load_execution_verification_migration_snapshot(42_161, WALLET)
15646 .await
15647 .unwrap();
15648 let nonce_verification = required_verification(
15649 client
15650 .verification
15651 .verify_transaction_count(&Address::from_str(WALLET).unwrap(), FIXTURE_BLOCK + 1)
15652 .await,
15653 "test migration nonce",
15654 )
15655 .unwrap();
15656 let headers = [
15657 VerifiedBlockHeader {
15658 number: FIXTURE_BLOCK,
15659 hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15660 parent_hash: B256::from_str(
15661 "0x0000000000000000000000000000000000000000000000000000000000000001",
15662 )
15663 .unwrap(),
15664 timestamp: FIXTURE_BLOCK_TIMESTAMP,
15665 base_fee_per_gas: Some(100_000_000),
15666 },
15667 VerifiedBlockHeader {
15668 number: FIXTURE_BLOCK + 1,
15669 hash: B256::from([0x22; 32]),
15670 parent_hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
15671 timestamp: FIXTURE_BLOCK_TIMESTAMP + 1,
15672 base_fee_per_gas: Some(100_000_000),
15673 },
15674 ];
15675 let migration = client
15676 .build_execution_verification_migration(
15677 snapshot,
15678 headers[1],
15679 &headers,
15680 &nonce_verification,
15681 )
15682 .await
15683 .unwrap();
15684 let finalized_headers = headers
15685 .iter()
15686 .map(|header| ExecutionVerifiedHeader {
15687 number: header.number,
15688 hash: header.hash.to_string(),
15689 parent_hash: header.parent_hash.to_string(),
15690 timestamp: header.timestamp,
15691 base_fee_per_gas: header.base_fee_per_gas,
15692 })
15693 .collect::<Vec<_>>();
15694 let decisions = [verification_decision(
15695 &nonce_verification,
15696 Some(FIXTURE_BLOCK + 1),
15697 Some(FIXTURE_BLOCK + 1),
15698 )];
15699
15700 initialize_test_verification_migration(
15701 &client,
15702 &finalized_headers,
15703 8,
15704 &decisions,
15705 &migration,
15706 )
15707 .await;
15708
15709 let migrated = database.get_execution_intent(intent.id).await.unwrap();
15710 let resume = database
15711 .load_execution_verification_resume(
15712 42_161,
15713 WALLET,
15714 &client
15715 .config
15716 .verification
15717 .as_ref()
15718 .unwrap()
15719 .manifest_version,
15720 &client.config.verification.as_ref().unwrap().manifest_digest,
15721 )
15722 .await
15723 .unwrap()
15724 .unwrap();
15725 let evidence: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15726 "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
15727 WHERE intent_id = $1 AND decision_class = 'migration'"
15728 )))
15729 .bind(intent.id)
15730 .fetch_one(&admin_pool)
15731 .await
15732 .unwrap();
15733
15734 assert_eq!(migrated.status, "finalized");
15735 assert!(migrated.active);
15736 assert_eq!(resume.next_canonical_nonce, 8);
15737 assert!(evidence >= 5);
15738
15739 drop_execution_schema(&admin_pool, &schema).await;
15740 }
15741
15742 #[tokio::test]
15743 async fn verification_resume_accepts_one_consumed_owned_nonce() {
15744 let Some((admin_pool, schema, client, _)) = execution_client_with_database(
15745 "verification_resume_owned_nonce_test",
15746 ready_rpc_state(),
15747 )
15748 .await
15749 else {
15750 return;
15751 };
15752 let database = client.cache.database.as_ref().unwrap();
15753 persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15754 let resume = database
15755 .load_execution_verification_resume(
15756 42_161,
15757 WALLET,
15758 &client
15759 .config
15760 .verification
15761 .as_ref()
15762 .unwrap()
15763 .manifest_version,
15764 &client.config.verification.as_ref().unwrap().manifest_digest,
15765 )
15766 .await
15767 .unwrap()
15768 .unwrap();
15769
15770 ensure_test_verification_ledger(&client, &resume.finalized_headers, 7, 8)
15771 .await
15772 .unwrap();
15773
15774 let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15775 "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15776 )))
15777 .fetch_one(&admin_pool)
15778 .await
15779 .unwrap();
15780 assert_eq!(nonce_state, (7, 0));
15781
15782 drop_execution_schema(&admin_pool, &schema).await;
15783 }
15784
15785 #[tokio::test]
15786 async fn verification_resume_rejects_unowned_nonce_advance() {
15787 for (test_name, mutation, observed_canonical_nonce) in [
15788 ("verification_resume_no_active_test", "no_active", 8),
15789 ("verification_resume_excess_test", "unchanged", 9),
15790 ("verification_resume_signed_test", "signed", 8),
15791 ("verification_resume_prepared_test", "prepared", 8),
15792 (
15793 "verification_resume_missing_payload_test",
15794 "missing_payload",
15795 8,
15796 ),
15797 ] {
15798 let Some((admin_pool, schema, client, _)) =
15799 execution_client_with_database(test_name, ready_rpc_state()).await
15800 else {
15801 return;
15802 };
15803 let database = client.cache.database.as_ref().unwrap();
15804 let (intent, _, _) =
15805 persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
15806
15807 match mutation {
15808 "no_active" => {
15809 sqlx::query(sqlx::AssertSqlSafe(format!(
15810 "UPDATE {schema}.execution_intent SET active = FALSE WHERE id = $1"
15811 )))
15812 .bind(intent.id)
15813 .execute(&admin_pool)
15814 .await
15815 .unwrap();
15816 }
15817 "signed" => {
15818 sqlx::query(sqlx::AssertSqlSafe(format!(
15819 "UPDATE {schema}.execution_intent SET status = 'signed' WHERE id = $1"
15820 )))
15821 .bind(intent.id)
15822 .execute(&admin_pool)
15823 .await
15824 .unwrap();
15825 sqlx::query(sqlx::AssertSqlSafe(format!(
15826 "UPDATE {schema}.execution_transaction_hash SET status = 'signed' \
15827 WHERE intent_id = $1"
15828 )))
15829 .bind(intent.id)
15830 .execute(&admin_pool)
15831 .await
15832 .unwrap();
15833 }
15834 "prepared" => {
15835 sqlx::query(sqlx::AssertSqlSafe(format!(
15836 "UPDATE {schema}.execution_intent \
15837 SET nonce = NULL, status = 'prepared' WHERE id = $1"
15838 )))
15839 .bind(intent.id)
15840 .execute(&admin_pool)
15841 .await
15842 .unwrap();
15843 sqlx::query(sqlx::AssertSqlSafe(format!(
15844 "UPDATE {schema}.execution_transaction_hash SET current = FALSE \
15845 WHERE intent_id = $1"
15846 )))
15847 .bind(intent.id)
15848 .execute(&admin_pool)
15849 .await
15850 .unwrap();
15851 }
15852 "missing_payload" => {
15853 sqlx::query(sqlx::AssertSqlSafe(format!(
15854 "UPDATE {schema}.execution_transaction_hash \
15855 SET payload_expected = FALSE, sealed_transaction = NULL \
15856 WHERE intent_id = $1"
15857 )))
15858 .bind(intent.id)
15859 .execute(&admin_pool)
15860 .await
15861 .unwrap();
15862 }
15863 "unchanged" => {}
15864 _ => unreachable!(),
15865 }
15866 let resume = database
15867 .load_execution_verification_resume(
15868 42_161,
15869 WALLET,
15870 &client
15871 .config
15872 .verification
15873 .as_ref()
15874 .unwrap()
15875 .manifest_version,
15876 &client.config.verification.as_ref().unwrap().manifest_digest,
15877 )
15878 .await
15879 .unwrap()
15880 .unwrap();
15881
15882 let error = ensure_test_verification_ledger(
15883 &client,
15884 &resume.finalized_headers,
15885 7,
15886 observed_canonical_nonce,
15887 )
15888 .await
15889 .unwrap_err();
15890
15891 assert!(
15892 error
15893 .to_string()
15894 .contains(if observed_canonical_nonce == 9 {
15895 "outside the owned recovery range"
15896 } else {
15897 "without"
15898 }),
15899 "{mutation}: {error}"
15900 );
15901 let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
15902 "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
15903 )))
15904 .fetch_one(&admin_pool)
15905 .await
15906 .unwrap();
15907 assert_eq!(nonce_state, (7, 0), "{mutation}");
15908
15909 drop_execution_schema(&admin_pool, &schema).await;
15910 }
15911 }
15912
15913 #[tokio::test]
15914 async fn restart_marks_prepared_intent_recoverable_without_broadcast() {
15915 let Some((admin_pool, schema, client, state)) =
15916 execution_client_with_database("execution_prepared_restart_test", ready_rpc_state())
15917 .await
15918 else {
15919 return;
15920 };
15921 let database = client.cache.database.as_ref().unwrap();
15922 let intent = reserve_test_wrap_intent(database).await;
15923 *client.in_flight.lock() = Some(InFlightSlot::Preparing(TransactionPurpose::Wrap));
15924
15925 client.reconcile_unresolved_execution().await.unwrap();
15926
15927 let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
15928 "SELECT status, active FROM {schema}.execution_intent WHERE id = {}",
15929 intent.id
15930 )))
15931 .fetch_one(&admin_pool)
15932 .await
15933 .unwrap();
15934 let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
15935 "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
15936 )))
15937 .fetch_all(&admin_pool)
15938 .await
15939 .unwrap();
15940
15941 assert_eq!(status, "recoverable");
15942 assert!(!active);
15943 assert_eq!(transitions, ["prepared", "recoverable"]);
15944 assert!(client.in_flight.lock().is_none());
15945 assert!(
15946 state
15947 .recorded_requests()
15948 .iter()
15949 .all(|request| request["method"] != "eth_sendRawTransaction")
15950 );
15951
15952 drop_execution_schema(&admin_pool, &schema).await;
15953 }
15954
15955 #[tokio::test]
15956 async fn protected_restart_authenticates_envelope_before_recovery() {
15957 let initial_state = broadcast_rpc_state()
15958 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
15959 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
15960 let Some((admin_pool, schema, mut first_client, _)) =
15961 execution_client_with_database("protected_restart_test", initial_state).await
15962 else {
15963 return;
15964 };
15965 let database = first_client.cache.database.as_ref().unwrap().clone();
15966 let value = U256::from(1_000_000_000_000_000_u64);
15967 let expected_hash = expected_wrap_tx_hash(value).await;
15968
15969 let error = first_client.wrap(value).await.unwrap_err();
15970
15971 assert!(error.to_string().contains("Timed out awaiting finality"));
15972 let intent = database
15973 .get_active_execution_intent(42161, WALLET)
15974 .await
15975 .unwrap()
15976 .unwrap();
15977 let payload = database
15978 .get_execution_transaction_hashes(intent.id)
15979 .await
15980 .unwrap()
15981 .pop()
15982 .unwrap();
15983 assert!(payload.raw_transaction.is_none());
15984 assert!(payload.sealed_transaction.is_some());
15985 let keys = first_client.payload_keys.take().unwrap();
15986 drop(first_client);
15987
15988 let block = finalized_wrap_block(expected_hash);
15989 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
15990 let restart_state = with_finalized_identity(
15991 execution_rpc_state()
15992 .with_response("eth_getTransactionReceipt", &receipt)
15993 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
15994 .with_response_sequence(
15995 "eth_call",
15996 &[
15997 CALL_BALANCE,
15998 CALL_BALANCE,
15999 CALL_BALANCE,
16000 CALL_BALANCE_AFTER_WRAP,
16001 CALL_BALANCE_AFTER_WRAP,
16002 CALL_BALANCE_AFTER_WRAP,
16003 ],
16004 ),
16005 &block,
16006 &receipt,
16007 );
16008 let addr = start_mock_rpc_server(restart_state.clone()).await;
16009 let mut restarted = test_client(format!("http://{addr}"));
16010 restarted.cache.database = Some(database);
16011 restarted.payload_keys = Some(keys);
16012 restarted.signer = Some(Arc::new(
16013 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16014 ));
16015
16016 restarted.reconcile_unresolved_execution().await.unwrap();
16017
16018 let record = restarted
16019 .cache
16020 .get_execution_transaction(42161, &expected_hash.to_string())
16021 .await
16022 .unwrap()
16023 .unwrap();
16024 let requests = restart_state.recorded_requests();
16025 assert_eq!(record.status, "finalized");
16026 assert!(restarted.in_flight.lock().is_none());
16027 assert_eq!(
16028 requests
16029 .iter()
16030 .filter(|request| request["method"] == "eth_sendRawTransaction")
16031 .count(),
16032 0
16033 );
16034
16035 drop_execution_schema(&admin_pool, &schema).await;
16036 }
16037
16038 #[tokio::test]
16039 async fn active_intent_reconciliation_waits_for_reservation_fence() {
16040 let Some((admin_pool, schema, client, _)) = execution_client_with_database(
16041 "execution_active_intent_reservation_fence_test",
16042 ready_rpc_state(),
16043 )
16044 .await
16045 else {
16046 return;
16047 };
16048 let database = client.cache.database.as_ref().unwrap().clone();
16049 let lock = PgAdvisoryLock::new(format!(
16050 "nautilus:blockchain:execution:42161:{}",
16051 WALLET.to_ascii_lowercase()
16052 ));
16053 let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
16054 unreachable!("string advisory locks use the 64-bit key space");
16055 };
16056 let mut reservation = admin_pool.begin().await.unwrap();
16057 sqlx::query("SELECT pg_advisory_xact_lock($1)")
16058 .bind(*lock_key)
16059 .execute(&mut *reservation)
16060 .await
16061 .unwrap();
16062 sqlx::query(sqlx::AssertSqlSafe(format!(
16063 "INSERT INTO {schema}.execution_intent (\
16064 schema_version, chain_id, wallet_address, purpose, status, transaction_to, \
16065 transaction_input, transaction_value, created_block\
16066 ) VALUES (2, $1, $2, 'wrap', 'prepared', $3, '0xd0e30db0', '1', $4)"
16067 )))
16068 .bind(42161_i32)
16069 .bind(WALLET)
16070 .bind(WETH_ADDRESS.to_string())
16071 .bind(i64::try_from(FIXTURE_BLOCK).unwrap())
16072 .execute(&mut *reservation)
16073 .await
16074 .unwrap();
16075
16076 let mut reconciliation = Box::pin(database.get_active_execution_intent(42161, WALLET));
16077 assert!(
16078 tokio::time::timeout(Duration::from_millis(100), &mut reconciliation)
16079 .await
16080 .is_err(),
16081 "reconciliation did not wait for the reservation fence"
16082 );
16083
16084 reservation.commit().await.unwrap();
16085 let intent = tokio::time::timeout(Duration::from_secs(2), reconciliation)
16086 .await
16087 .unwrap()
16088 .unwrap()
16089 .unwrap();
16090
16091 assert_eq!(intent.chain_id, 42161);
16092 assert_eq!(intent.wallet_address, WALLET);
16093 assert_eq!(intent.purpose, "wrap");
16094 assert_eq!(intent.status, "prepared");
16095 assert!(intent.active);
16096
16097 drop_execution_schema(&admin_pool, &schema).await;
16098 }
16099
16100 #[tokio::test]
16101 async fn restart_keeps_unbroadcast_signed_intent_reserved() {
16102 let Some((admin_pool, schema, client, state)) =
16103 execution_client_with_database("execution_signed_restart_test", ready_rpc_state())
16104 .await
16105 else {
16106 return;
16107 };
16108 let database = client.cache.database.as_ref().unwrap();
16109 let intent = reserve_test_wrap_intent(database).await;
16110 let transaction = build_eip1559_transaction(
16111 42161,
16112 7,
16113 78_000,
16114 130_000_000,
16115 10_000_000,
16116 WETH_ADDRESS,
16117 U256::from(1u64),
16118 Bytes::from(nautilus_core::hex::decode("d0e30db0").unwrap()),
16119 );
16120 let (tx_hash, raw_transaction) = sign_eip1559_transaction(
16121 transaction,
16122 &PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16123 )
16124 .await
16125 .unwrap();
16126 database
16127 .assign_execution_intent_nonce(intent.id, 7)
16128 .await
16129 .unwrap();
16130 let intent = database.get_execution_intent(intent.id).await.unwrap();
16131 persist_test_payload(
16132 database,
16133 client.payload_keys.as_deref(),
16134 &intent,
16135 tx_hash,
16136 &raw_transaction,
16137 )
16138 .await;
16139
16140 let error = client.reconcile_unresolved_execution().await.unwrap_err();
16141
16142 assert!(
16143 error
16144 .to_string()
16145 .contains("was not authorized for broadcast"),
16146 "was: {error}"
16147 );
16148 let recovery = recovering_in_flight(&client);
16149 assert_eq!(recovery.intent_id, intent.id);
16150 assert_eq!(recovery.nonce, 7);
16151 assert_eq!(recovery.purpose, TransactionPurpose::Wrap);
16152 assert_eq!(
16153 execution_intent_markers(&admin_pool, &schema).await,
16154 vec![("wrap".into(), "signed".into(), false, true)]
16155 );
16156 assert!(
16157 state
16158 .recorded_requests()
16159 .iter()
16160 .all(|request| request["method"] != "eth_sendRawTransaction")
16161 );
16162
16163 drop_execution_schema(&admin_pool, &schema).await;
16164 }
16165
16166 #[tokio::test]
16167 async fn restart_quarantines_legacy_recoverable_signed_intent() {
16168 let Some((admin_pool, schema, client, state)) = execution_client_with_database(
16169 "execution_legacy_recoverable_restart_test",
16170 ready_rpc_state(),
16171 )
16172 .await
16173 else {
16174 return;
16175 };
16176 let database = client.cache.database.as_ref().unwrap();
16177 let intent = reserve_test_wrap_intent(database).await;
16178 let tx_hash = B256::from([0x55; 32]);
16179 database
16180 .assign_execution_intent_nonce(intent.id, 7)
16181 .await
16182 .unwrap();
16183 let intent = database.get_execution_intent(intent.id).await.unwrap();
16184 persist_test_payload(
16185 database,
16186 client.payload_keys.as_deref(),
16187 &intent,
16188 tx_hash,
16189 &[0x01, 0x02, 0x03],
16190 )
16191 .await;
16192 sqlx::query(sqlx::AssertSqlSafe(format!(
16193 "UPDATE {schema}.execution_intent SET status = 'recoverable', active = FALSE WHERE id = {}",
16194 intent.id
16195 )))
16196 .execute(&admin_pool)
16197 .await
16198 .unwrap();
16199
16200 let error = client.reconcile_unresolved_execution().await.unwrap_err();
16201
16202 assert!(
16203 error
16204 .to_string()
16205 .contains("retains signed transaction bytes"),
16206 "was: {error}"
16207 );
16208 assert!(client.in_flight.lock().is_none());
16209 assert!(
16210 state
16211 .recorded_requests()
16212 .iter()
16213 .all(|request| request["method"] != "eth_sendRawTransaction")
16214 );
16215
16216 drop_execution_schema(&admin_pool, &schema).await;
16217 }
16218
16219 #[rstest]
16220 #[case::current("execution_invalid_signed_restart_test", false, "broadcast")]
16221 #[case::historical("execution_invalid_historical_restart_test", true, "replaced")]
16222 #[tokio::test]
16223 async fn restart_rejects_invalid_signed_bytes_before_recovery_effects(
16224 #[case] test_name: &str,
16225 #[case] historical: bool,
16226 #[case] expected_status: &str,
16227 ) {
16228 let Some((admin_pool, schema, first_client, state, _)) =
16229 swap_client_with_database(test_name, ready_rpc_state()).await
16230 else {
16231 return;
16232 };
16233 let database = first_client.cache.database.as_ref().unwrap().clone();
16234 let (intent, _) =
16235 persist_invalid_test_swap(&database, first_client.payload_keys.as_deref()).await;
16236
16237 if historical {
16238 sqlx::query(sqlx::AssertSqlSafe(format!(
16239 "UPDATE {schema}.execution_transaction_hash \
16240 SET current = FALSE, status = 'replaced' WHERE intent_id = $1"
16241 )))
16242 .bind(intent.id)
16243 .execute(&admin_pool)
16244 .await
16245 .unwrap();
16246 sqlx::query(sqlx::AssertSqlSafe(format!(
16247 "INSERT INTO {schema}.execution_transaction_hash (\
16248 intent_id, chain_id, transaction_hash, payload_expected, status, current\
16249 ) VALUES ($1, 42161, $2, FALSE, 'replaced', TRUE)"
16250 )))
16251 .bind(intent.id)
16252 .bind(B256::from([0x44; 32]).to_string())
16253 .execute(&admin_pool)
16254 .await
16255 .unwrap();
16256 sqlx::query(sqlx::AssertSqlSafe(format!(
16257 "UPDATE {schema}.execution_intent SET status = 'replaced' WHERE id = $1"
16258 )))
16259 .bind(intent.id)
16260 .execute(&admin_pool)
16261 .await
16262 .unwrap();
16263 }
16264 let transitions_before: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16265 "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16266 )))
16267 .fetch_one(&admin_pool)
16268 .await
16269 .unwrap();
16270 let config = first_client.config.clone();
16271 let payload_keys = first_client.payload_keys.clone();
16272 drop(first_client);
16273
16274 let (mut restarted, _) = swap_client_with_cache(config);
16275 restarted.cache.database = Some(database);
16276 restarted.payload_keys = payload_keys;
16277 restarted.signer = Some(Arc::new(
16278 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16279 ));
16280 let mut receiver = start_with_events(&mut restarted);
16281
16282 let error = restarted
16283 .reconcile_unresolved_execution()
16284 .await
16285 .unwrap_err();
16286
16287 let recovery = recovering_in_flight(&restarted);
16288 let (status, acknowledgement_emitted, active): (String, bool, bool) =
16289 sqlx::query_as(sqlx::AssertSqlSafe(format!(
16290 "SELECT status, acknowledgement_emitted, active FROM {schema}.execution_intent"
16291 )))
16292 .fetch_one(&admin_pool)
16293 .await
16294 .unwrap();
16295 let transitions_after: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16296 "SELECT COUNT(*) FROM {schema}.execution_transaction_transition"
16297 )))
16298 .fetch_one(&admin_pool)
16299 .await
16300 .unwrap();
16301
16302 assert!(
16303 error
16304 .to_string()
16305 .contains("is not a complete EIP-2718 envelope"),
16306 "was: {error}"
16307 );
16308 assert_eq!(recovery.intent_id, intent.id);
16309 assert_eq!(recovery.nonce, 7);
16310 assert_eq!(recovery.purpose, TransactionPurpose::Swap);
16311 assert_eq!(status, expected_status);
16312 assert!(!acknowledgement_emitted);
16313 assert!(active);
16314 assert_eq!(transitions_after, transitions_before);
16315 assert!(collect_order_events(&mut receiver).is_empty());
16316 assert!(
16317 state
16318 .recorded_requests()
16319 .iter()
16320 .all(|request| request["method"] != "eth_sendRawTransaction")
16321 );
16322
16323 drop_execution_schema(&admin_pool, &schema).await;
16324 }
16325
16326 #[tokio::test]
16327 async fn restart_rebroadcasts_only_durably_authorized_bytes() {
16328 let Some((admin_pool, schema, first_client, _)) =
16329 execution_client_with_database("execution_broadcast_restart_test", ready_rpc_state())
16330 .await
16331 else {
16332 return;
16333 };
16334 let database = first_client.cache.database.as_ref().unwrap().clone();
16335 let (_, _, raw_tx) =
16336 persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16337 let payload_keys = first_client.payload_keys.clone();
16338 drop(first_client);
16339
16340 let restart_state = execution_rpc_state()
16341 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16342 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16343 .with_response("eth_call", CALL_EMPTY)
16344 .with_send_raw_transaction_echo();
16345 let addr = start_mock_rpc_server(restart_state.clone()).await;
16346 let mut restarted = test_client(format!("http://{addr}"));
16347 restarted.cache.database = Some(database);
16348 restarted.payload_keys = payload_keys;
16349 restarted.signer = Some(Arc::new(
16350 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16351 ));
16352
16353 restarted.reconcile_unresolved_execution().await.unwrap();
16354
16355 let broadcasts = restart_state
16356 .recorded_requests()
16357 .into_iter()
16358 .filter(|request| request["method"] == "eth_sendRawTransaction")
16359 .collect::<Vec<_>>();
16360 assert_eq!(broadcasts.len(), 1);
16361 assert_eq!(broadcasts[0]["params"][0], hex::encode_prefixed(&raw_tx));
16362 assert_eq!(
16363 execution_intent_markers(&admin_pool, &schema).await,
16364 vec![("wrap".into(), "dropped".into(), false, true)]
16365 );
16366
16367 drop_execution_schema(&admin_pool, &schema).await;
16368 }
16369
16370 #[tokio::test]
16371 async fn restart_suppresses_rebroadcast_when_canonical_nonce_advanced() {
16372 let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16373 "execution_rebroadcast_nonce_advanced_test",
16374 ready_rpc_state(),
16375 )
16376 .await
16377 else {
16378 return;
16379 };
16380 let database = first_client.cache.database.as_ref().unwrap().clone();
16381 let (intent, _, _) =
16382 persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16383 let payload_keys = first_client.payload_keys.clone();
16384 drop(first_client);
16385
16386 let mut empty_block: serde_json::Value =
16387 serde_json::from_str(&replacement_head_block(B256::from([0x44; 32]))).unwrap();
16388 empty_block["result"]["transactions"] = serde_json::json!([]);
16389 let empty_block = empty_block.to_string();
16390 let restart_state = execution_rpc_state()
16391 .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16392 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16393 .with_response("eth_call", CALL_EMPTY)
16394 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &empty_block)
16395 .with_send_raw_transaction_echo();
16396 let addr = start_mock_rpc_server(restart_state.clone()).await;
16397 let mut restarted = test_client(format!("http://{addr}"));
16398 restarted.cache.database = Some(database);
16399 restarted.payload_keys = payload_keys;
16400 restarted.signer = Some(Arc::new(
16401 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16402 ));
16403
16404 let error = restarted
16405 .reconcile_unresolved_execution()
16406 .await
16407 .unwrap_err();
16408
16409 let requests = restart_state.recorded_requests();
16410 assert!(
16411 requests
16412 .iter()
16413 .all(|request| request["method"] != "eth_sendRawTransaction")
16414 );
16415 assert!(requests.iter().all(|request| {
16416 request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16417 }));
16418 let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16419 "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16420 WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16421 )))
16422 .bind(intent.id)
16423 .fetch_one(&admin_pool)
16424 .await
16425 .unwrap();
16426 assert!(
16427 error
16428 .to_string()
16429 .contains("without an authenticated signer transaction"),
16430 "was: {error}"
16431 );
16432 assert_eq!(decision_count, 6);
16433 let replacement_decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16434 "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16435 WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
16436 )))
16437 .bind(intent.id)
16438 .fetch_one(&admin_pool)
16439 .await
16440 .unwrap();
16441 assert_eq!(replacement_decision_count, 2);
16442 let nonce_state = sqlx::query_as::<_, (i64, i64)>(sqlx::AssertSqlSafe(format!(
16443 "SELECT next_canonical_nonce, revision FROM {schema}.execution_verification_nonce"
16444 )))
16445 .fetch_one(&admin_pool)
16446 .await
16447 .unwrap();
16448 assert_eq!(nonce_state, (7, 0));
16449
16450 drop_execution_schema(&admin_pool, &schema).await;
16451 }
16452
16453 #[tokio::test]
16454 async fn restart_suppresses_rebroadcast_when_receipt_exists() {
16455 let Some((admin_pool, schema, first_client, _)) = execution_client_with_database(
16456 "execution_rebroadcast_receipt_present_test",
16457 ready_rpc_state(),
16458 )
16459 .await
16460 else {
16461 return;
16462 };
16463 let database = first_client.cache.database.as_ref().unwrap().clone();
16464 let (intent, _, _) =
16465 persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16466 let payload_keys = first_client.payload_keys.clone();
16467 drop(first_client);
16468
16469 let restart_state = execution_rpc_state()
16470 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16471 .with_response("eth_getTransactionReceipt", RECEIPT_SUCCESS)
16472 .with_response("eth_call", CALL_EMPTY)
16473 .with_send_raw_transaction_echo();
16474 let addr = start_mock_rpc_server(restart_state.clone()).await;
16475 let mut restarted = test_client(format!("http://{addr}"));
16476 restarted.cache.database = Some(database);
16477 restarted.payload_keys = payload_keys;
16478 restarted.signer = Some(Arc::new(
16479 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16480 ));
16481
16482 let error = restarted
16483 .reconcile_unresolved_execution()
16484 .await
16485 .unwrap_err();
16486
16487 assert!(
16488 error
16489 .to_string()
16490 .contains("finalized transaction verification is locally invalid"),
16491 "was: {error}"
16492 );
16493 let requests = restart_state.recorded_requests();
16494 assert!(
16495 requests
16496 .iter()
16497 .all(|request| request["method"] != "eth_sendRawTransaction")
16498 );
16499 assert!(requests.iter().all(|request| {
16500 request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16501 }));
16502 let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16503 "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16504 WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16505 )))
16506 .bind(intent.id)
16507 .fetch_one(&admin_pool)
16508 .await
16509 .unwrap();
16510 assert_eq!(decision_count, 6);
16511
16512 drop_execution_schema(&admin_pool, &schema).await;
16513 }
16514
16515 #[rstest]
16516 #[case("execution_rebroadcast_false_test", CALL_ZERO)]
16517 #[case("execution_rebroadcast_revert_test", CALL_REVERTED)]
16518 #[tokio::test]
16519 async fn restart_suppresses_rebroadcast_when_simulation_denies(
16520 #[case] test_name: &str,
16521 #[case] simulation_response: &str,
16522 ) {
16523 let Some((admin_pool, schema, first_client, _)) =
16524 execution_client_with_database(test_name, ready_rpc_state()).await
16525 else {
16526 return;
16527 };
16528 let database = first_client.cache.database.as_ref().unwrap().clone();
16529 let (intent, _, _) =
16530 persist_test_wrap_broadcast(&database, first_client.payload_keys.as_deref()).await;
16531 let payload_keys = first_client.payload_keys.clone();
16532 drop(first_client);
16533
16534 let restart_state = execution_rpc_state()
16535 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
16536 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16537 .with_response("eth_call", simulation_response)
16538 .with_send_raw_transaction_echo();
16539 let addr = start_mock_rpc_server(restart_state.clone()).await;
16540 let mut restarted = test_client(format!("http://{addr}"));
16541 restarted.cache.database = Some(database);
16542 restarted.payload_keys = payload_keys;
16543 restarted.signer = Some(Arc::new(
16544 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16545 ));
16546
16547 restarted.reconcile_unresolved_execution().await.unwrap();
16548
16549 let requests = restart_state.recorded_requests();
16550 assert!(
16551 requests
16552 .iter()
16553 .all(|request| request["method"] != "eth_sendRawTransaction")
16554 );
16555 let simulation_calls = requests
16556 .iter()
16557 .filter(|request| {
16558 request["method"] == "eth_call"
16559 && request["params"][0]["data"] == "0xd0e30db0"
16560 && request["params"][1] == FIXTURE_BLOCK_PARAM
16561 })
16562 .count();
16563 assert_eq!(simulation_calls, 3);
16564 let decision_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16565 "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
16566 WHERE intent_id = $1 AND decision_class = 'rebroadcast'"
16567 )))
16568 .bind(intent.id)
16569 .fetch_one(&admin_pool)
16570 .await
16571 .unwrap();
16572 assert_eq!(decision_count, 7);
16573
16574 drop_execution_schema(&admin_pool, &schema).await;
16575 }
16576
16577 #[tokio::test]
16578 async fn restart_wrap_identity_mismatch_keeps_signer_ownership() {
16579 let initial_state = broadcast_rpc_state()
16580 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16581 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16582 let Some((admin_pool, schema, mut first_client, _)) =
16583 execution_client_with_database("execution_restart_mismatch_test", initial_state).await
16584 else {
16585 return;
16586 };
16587 let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16588 let error = first_client
16589 .wrap(U256::from(1_000_000_000_000_000_u64))
16590 .await
16591 .unwrap_err();
16592 assert!(
16593 error.to_string().contains("Timed out awaiting finality"),
16594 "was: {error}"
16595 );
16596 let database = first_client.cache.database.as_ref().unwrap().clone();
16597 let payload_keys = first_client.payload_keys.clone();
16598 drop(first_client);
16599
16600 let mismatched_block = serde_json::json!({
16602 "jsonrpc": "2.0",
16603 "id": 1,
16604 "result": {
16605 "number": "0x1cf0d41",
16606 "hash": "0x2222222222222222222222222222222222222222222222222222222222222222",
16607 "parentHash": FIXTURE_BLOCK_HASH,
16608 "timestamp": "0x69044a21",
16609 "baseFeePerGas": "0x5f5e100",
16610 "transactions": [{
16611 "hash": expected_hash.to_string(),
16612 "from": WALLET,
16613 "nonce": "0x7",
16614 "chainId": "0xa4b1",
16615 "type": "0x2",
16616 "to": WETH,
16617 "input": "0xd0e30db0",
16618 "value": "0x0",
16619 "gas": "0x130b0",
16620 "maxFeePerGas": "0x7bfa480",
16621 "maxPriorityFeePerGas": "0x989680"
16622 }]
16623 }
16624 })
16625 .to_string();
16626 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16627 let restart_state = with_finalized_identity(
16628 execution_rpc_state()
16629 .with_response("eth_getTransactionReceipt", &receipt)
16630 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &mismatched_block),
16631 &mismatched_block,
16632 &receipt,
16633 );
16634 let addr = start_mock_rpc_server(restart_state.clone()).await;
16635 let mut restarted = test_client(format!("http://{addr}"));
16636 restarted.cache.database = Some(database);
16637 restarted.payload_keys = payload_keys;
16638 restarted.signer = Some(Arc::new(
16639 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16640 ));
16641
16642 let error = restarted
16643 .reconcile_unresolved_execution()
16644 .await
16645 .unwrap_err();
16646
16647 assert!(
16648 error
16649 .to_string()
16650 .contains("finalized transaction identity mismatch"),
16651 "was: {error}"
16652 );
16653 let in_flight = awaiting_in_flight(&restarted);
16654 assert_eq!(in_flight.nonce, 7);
16655 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16656 assert_eq!(in_flight.tx_hash, expected_hash);
16657 let requests = restart_state.recorded_requests();
16658 assert!(
16659 requests.iter().all(|request| {
16660 request["method"] != "eth_call"
16661 || !request["params"][0]["data"]
16662 .as_str()
16663 .is_some_and(|data| data.starts_with(BALANCE_OF_SELECTOR))
16664 }),
16665 "the postcondition must not run when call identity is unproven"
16666 );
16667 assert!(
16668 requests
16669 .iter()
16670 .all(|request| request["method"] != "eth_sendRawTransaction")
16671 );
16672 let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16673 "SELECT status, active FROM {schema}.execution_intent"
16674 )))
16675 .fetch_one(&admin_pool)
16676 .await
16677 .unwrap();
16678 assert_eq!(status, "dropped");
16679 assert!(active);
16680
16681 let database = restarted.cache.database.as_ref().unwrap().clone();
16682 let payload_keys = restarted.payload_keys.clone();
16683 drop(restarted);
16684 let mut second = test_client(format!("http://{addr}"));
16685 second.cache.database = Some(database);
16686 second.payload_keys = payload_keys;
16687 second.signer = Some(Arc::new(
16688 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16689 ));
16690 let error = second.reconcile_unresolved_execution().await.unwrap_err();
16691 assert!(
16692 error
16693 .to_string()
16694 .contains("finalized transaction identity mismatch"),
16695 "was: {error}"
16696 );
16697 let active: bool = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
16698 "SELECT active FROM {schema}.execution_intent"
16699 )))
16700 .fetch_one(&admin_pool)
16701 .await
16702 .unwrap();
16703 assert!(active);
16704
16705 drop_execution_schema(&admin_pool, &schema).await;
16706 }
16707
16708 #[tokio::test]
16709 async fn restart_wrap_postcondition_failure_keeps_signer_ownership() {
16710 let initial_state = broadcast_rpc_state()
16711 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16712 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16713 let Some((admin_pool, schema, mut first_client, _)) =
16714 execution_client_with_database("execution_restart_postcondition_test", initial_state)
16715 .await
16716 else {
16717 return;
16718 };
16719 let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16720 let error = first_client
16721 .wrap(U256::from(1_000_000_000_000_000_u64))
16722 .await
16723 .unwrap_err();
16724 assert!(
16725 error.to_string().contains("Timed out awaiting finality"),
16726 "was: {error}"
16727 );
16728 let database = first_client.cache.database.as_ref().unwrap().clone();
16729 let payload_keys = first_client.payload_keys.clone();
16730 drop(first_client);
16731
16732 let failing_wrap_state = || {
16734 let block = finalized_wrap_block(expected_hash);
16735 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
16736 with_finalized_identity(
16737 execution_rpc_state()
16738 .with_response("eth_getTransactionReceipt", &receipt)
16739 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
16740 .with_response_sequence("eth_call", &[CALL_BALANCE; 6]),
16741 &block,
16742 &receipt,
16743 )
16744 };
16745 let restart_state = failing_wrap_state();
16746 let addr = start_mock_rpc_server(restart_state.clone()).await;
16747 let mut restarted = test_client(format!("http://{addr}"));
16748 restarted.cache.database = Some(database);
16749 restarted.payload_keys = payload_keys;
16750 restarted.signer = Some(Arc::new(
16751 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16752 ));
16753
16754 let error = restarted
16755 .reconcile_unresolved_execution()
16756 .await
16757 .unwrap_err();
16758
16759 assert!(
16760 error.to_string().contains("did not increase by"),
16761 "was: {error}"
16762 );
16763 let in_flight = awaiting_in_flight(&restarted);
16764 assert_eq!(in_flight.nonce, 7);
16765 assert_eq!(in_flight.purpose, TransactionPurpose::Wrap);
16766 assert!(
16767 restart_state
16768 .recorded_requests()
16769 .iter()
16770 .all(|request| request["method"] != "eth_sendRawTransaction")
16771 );
16772 assert_eq!(
16773 execution_intent_markers(&admin_pool, &schema).await,
16774 vec![("wrap".into(), "dropped".into(), false, true)]
16775 );
16776
16777 let addr = start_mock_rpc_server(failing_wrap_state()).await;
16778 let error = later_reconnect(restarted, format!("http://{addr}")).await;
16779 assert!(
16780 error.to_string().contains("did not increase by"),
16781 "was: {error}"
16782 );
16783 assert_eq!(
16784 execution_intent_markers(&admin_pool, &schema).await,
16785 vec![("wrap".into(), "dropped".into(), false, true)]
16786 );
16787
16788 drop_execution_schema(&admin_pool, &schema).await;
16789 }
16790
16791 #[tokio::test]
16792 async fn restart_wrap_revert_marks_terminal_and_releases() {
16793 let initial_state = broadcast_rpc_state()
16794 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16795 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16796 let Some((admin_pool, schema, mut first_client, _)) =
16797 execution_client_with_database("execution_restart_wrap_revert_test", initial_state)
16798 .await
16799 else {
16800 return;
16801 };
16802 let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16803 let error = first_client
16804 .wrap(U256::from(1_000_000_000_000_000_u64))
16805 .await
16806 .unwrap_err();
16807 assert!(
16808 error.to_string().contains("Timed out awaiting finality"),
16809 "was: {error}"
16810 );
16811 let database = first_client.cache.database.as_ref().unwrap().clone();
16812 let payload_keys = first_client.payload_keys.clone();
16813 drop(first_client);
16814
16815 let block = finalized_wrap_block(expected_hash);
16816 let receipt = receipt_with_transaction_hash(RECEIPT_REVERTED, expected_hash);
16817 let restart_state = with_finalized_identity(
16818 execution_rpc_state()
16819 .with_response("eth_getTransactionReceipt", &receipt)
16820 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block),
16821 &block,
16822 &receipt,
16823 );
16824 let addr = start_mock_rpc_server(restart_state.clone()).await;
16825 let mut restarted = test_client(format!("http://{addr}"));
16826 restarted.cache.database = Some(database);
16827 restarted.payload_keys = payload_keys;
16828 restarted.signer = Some(Arc::new(
16829 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16830 ));
16831
16832 restarted.reconcile_unresolved_execution().await.unwrap();
16833
16834 assert!(restarted.in_flight.lock().is_none());
16835 assert!(
16836 restart_state
16837 .recorded_requests()
16838 .iter()
16839 .all(|request| request["method"] != "eth_sendRawTransaction")
16840 );
16841 assert_eq!(
16842 execution_intent_markers(&admin_pool, &schema).await,
16843 vec![("wrap".into(), "reverted".into(), true, false)]
16844 );
16845
16846 drop_execution_schema(&admin_pool, &schema).await;
16847 }
16848
16849 #[tokio::test]
16850 async fn restart_quarantines_unretained_same_nonce_wrap_replacement() {
16851 let initial_state = broadcast_rpc_state()
16852 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
16853 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE);
16854 let Some((admin_pool, schema, mut first_client, _)) =
16855 execution_client_with_database("execution_restart_replacement_test", initial_state)
16856 .await
16857 else {
16858 return;
16859 };
16860 let original_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000_u64)).await;
16861 let error = first_client
16862 .wrap(U256::from(1_000_000_000_000_000_u64))
16863 .await
16864 .unwrap_err();
16865 assert!(
16866 error.to_string().contains("Timed out awaiting finality"),
16867 "was: {error}"
16868 );
16869 let database = first_client.cache.database.as_ref().unwrap().clone();
16870 let payload_keys = first_client.payload_keys.clone();
16871 drop(first_client);
16872
16873 let replacement_hash = B256::from([0x44; 32]);
16875 let restart_state = execution_rpc_state()
16876 .with_response("eth_getTransactionCount", TRANSACTION_COUNT_NEXT)
16877 .with_response_sequence("eth_call", &[CALL_BALANCE, CALL_BALANCE_AFTER_WRAP])
16878 .with_response_sequence(
16879 "eth_getTransactionReceipt",
16880 &[RECEIPT_NULL, RECEIPT_NULL, RECEIPT_NULL],
16881 )
16882 .with_parameter_response(
16883 "eth_getBlockByNumber",
16884 "0x1cf0d40",
16885 &replacement_head_block(replacement_hash),
16886 )
16887 .with_parameter_response(
16888 "eth_getBlockByNumber",
16889 "0x1cf0d41",
16890 &finalized_wrap_block(replacement_hash),
16891 );
16892 let addr = start_mock_rpc_server(restart_state.clone()).await;
16893 let mut restarted = test_client(format!("http://{addr}"));
16894 restarted.cache.database = Some(database);
16895 restarted.payload_keys = payload_keys;
16896 restarted.signer = Some(Arc::new(
16897 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
16898 ));
16899 restarted.transaction_limits.receipt_timeout_secs = 2;
16900
16901 let error = restarted
16902 .reconcile_unresolved_execution()
16903 .await
16904 .unwrap_err();
16905
16906 let hashes: Vec<(String, String, bool)> = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16907 "SELECT transaction_hash, status, current FROM \
16908 {schema}.execution_transaction_hash ORDER BY id"
16909 )))
16910 .fetch_all(&admin_pool)
16911 .await
16912 .unwrap();
16913 let requests = restart_state.recorded_requests();
16914
16915 assert!(
16916 error
16917 .to_string()
16918 .contains("has no authenticated retained payload"),
16919 "was: {error}"
16920 );
16921 assert_eq!(
16922 hashes,
16923 [(original_hash.to_string(), "dropped".to_string(), true)]
16924 );
16925 assert!(restarted.in_flight.lock().is_some());
16926 assert_eq!(
16927 execution_intent_markers(&admin_pool, &schema).await,
16928 vec![("wrap".into(), "dropped".into(), false, true)]
16929 );
16930 assert_eq!(
16931 requests
16932 .iter()
16933 .filter(|request| request["method"] == "eth_getTransactionReceipt")
16934 .count(),
16935 3
16936 );
16937 assert!(requests.iter().all(|request| {
16938 request["method"] != "eth_call" || request["params"][0]["data"] != "0xd0e30db0"
16939 }));
16940 assert!(
16941 requests
16942 .iter()
16943 .all(|request| request["method"] != "eth_sendRawTransaction")
16944 );
16945
16946 drop_execution_schema(&admin_pool, &schema).await;
16947 }
16948
16949 #[tokio::test]
16950 async fn replacement_scan_accepts_only_an_authenticated_retained_payload() {
16951 let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
16952 let mut replacement_block: serde_json::Value =
16953 serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
16954 replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x1");
16955 let replacement_block = replacement_block.to_string();
16956 let state = execution_rpc_state().with_parameter_response(
16957 "eth_getBlockByNumber",
16958 "0x1cf0d40",
16959 &replacement_block,
16960 );
16961 let Some((admin_pool, schema, client, _)) =
16962 execution_client_with_database("replacement_scan_authenticated", state).await
16963 else {
16964 return;
16965 };
16966 let database = client.cache.database.as_ref().unwrap();
16967 let (intent, persisted_hash, persisted_raw) =
16968 persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
16969 assert_eq!(persisted_hash, expected_hash);
16970 let authenticated_payloads = HashMap::from([(expected_hash, persisted_raw.clone())]);
16971 let head = VerifiedBlockHeader {
16972 number: FIXTURE_BLOCK,
16973 hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
16974 parent_hash: B256::from_str(
16975 "0x0000000000000000000000000000000000000000000000000000000000000001",
16976 )
16977 .unwrap(),
16978 timestamp: FIXTURE_BLOCK_TIMESTAMP,
16979 base_fee_per_gas: Some(100_000_000),
16980 };
16981
16982 let matched = client
16983 .transaction_executor()
16984 .unwrap()
16985 .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
16986 .await
16987 .unwrap()
16988 .unwrap();
16989
16990 assert_eq!(matched, (expected_hash, persisted_raw));
16991 let cursor: (i64, String) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
16992 "SELECT finalized_cursor_number, finalized_cursor_hash \
16993 FROM {schema}.execution_replacement_scan WHERE intent_id = $1"
16994 )))
16995 .bind(intent.id)
16996 .fetch_one(&admin_pool)
16997 .await
16998 .unwrap();
16999 assert_eq!(
17000 cursor,
17001 (FIXTURE_BLOCK as i64, FIXTURE_BLOCK_HASH.to_string())
17002 );
17003 let evidence_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17004 "SELECT COUNT(*) FROM {schema}.execution_verification_decision \
17005 WHERE intent_id = $1 AND decision_class = 'replacement_scan'"
17006 )))
17007 .bind(intent.id)
17008 .fetch_one(&admin_pool)
17009 .await
17010 .unwrap();
17011 assert_eq!(evidence_count, 2);
17012
17013 drop_execution_schema(&admin_pool, &schema).await;
17014 }
17015
17016 #[tokio::test]
17017 async fn restart_reconciles_finalized_approve_after_validation() {
17018 let initial_state = broadcast_rpc_state()
17019 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
17020 .with_response("eth_call", CALL_BOOL_TRUE)
17021 .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
17022 .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
17023 let Some((admin_pool, schema, mut first_client, _)) =
17024 execution_client_with_database("execution_restart_approve_test", initial_state).await
17025 else {
17026 return;
17027 };
17028 let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
17029 let error = first_client
17030 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17031 .await
17032 .unwrap_err();
17033 assert!(
17034 error.to_string().contains("Timed out awaiting finality"),
17035 "was: {error}"
17036 );
17037 let database = first_client.cache.database.as_ref().unwrap().clone();
17038 let payload_keys = first_client.payload_keys.clone();
17039 drop(first_client);
17040
17041 let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
17042 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
17043 let restart_state = with_finalized_identity(
17044 execution_rpc_state()
17045 .with_response("eth_getTransactionReceipt", &receipt)
17046 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
17047 .with_call_response(ALLOWANCE_SELECTOR, CALL_ALLOWANCE_1000),
17048 &block,
17049 &receipt,
17050 );
17051 let addr = start_mock_rpc_server(restart_state.clone()).await;
17052 let mut restarted = test_client(format!("http://{addr}"));
17053 restarted.cache.database = Some(database);
17054 restarted.payload_keys = payload_keys;
17055 restarted.signer = Some(Arc::new(
17056 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
17057 ));
17058
17059 restarted.reconcile_unresolved_execution().await.unwrap();
17060
17061 let record = restarted
17062 .cache
17063 .get_execution_transaction(42161, &expected_hash.to_string())
17064 .await
17065 .unwrap()
17066 .unwrap();
17067 let requests = restart_state.recorded_requests();
17068 assert_eq!(record.status, "finalized");
17069 assert_eq!(record.purpose, "approve");
17070 assert!(restarted.in_flight.lock().is_none());
17071 assert_eq!(
17072 execution_intent_markers(&admin_pool, &schema).await,
17073 vec![("approve".into(), "finalized".into(), true, false)]
17074 );
17075 assert_eq!(
17076 requests
17077 .iter()
17078 .filter(|request| {
17079 request["method"] == "eth_call"
17080 && request["params"][0]["data"]
17081 .as_str()
17082 .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17083 })
17084 .count(),
17085 3
17086 );
17087 assert_eq!(
17088 requests
17089 .iter()
17090 .filter(|request| request["method"] == "eth_sendRawTransaction")
17091 .count(),
17092 0
17093 );
17094
17095 drop_execution_schema(&admin_pool, &schema).await;
17096 }
17097
17098 #[tokio::test]
17099 async fn restart_approve_postcondition_failure_keeps_signer_ownership() {
17100 let initial_state = broadcast_rpc_state()
17101 .with_response("eth_getTransactionReceipt", RECEIPT_NULL)
17102 .with_response("eth_call", CALL_BOOL_TRUE)
17103 .with_call_response_sequence(ALLOWANCE_SELECTOR, &[CALL_ZERO; 3])
17104 .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO);
17105 let Some((admin_pool, schema, mut first_client, _)) =
17106 execution_client_with_database("execution_restart_approve_post_test", initial_state)
17107 .await
17108 else {
17109 return;
17110 };
17111 let expected_hash = expected_approve_tx_hash(U256::from(1_000u64)).await;
17112 let error = first_client
17113 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17114 .await
17115 .unwrap_err();
17116 assert!(
17117 error.to_string().contains("Timed out awaiting finality"),
17118 "was: {error}"
17119 );
17120 let database = first_client.cache.database.as_ref().unwrap().clone();
17121 let payload_keys = first_client.payload_keys.clone();
17122 drop(first_client);
17123
17124 let block = finalized_approve_block(expected_hash, U256::from(1_000u64));
17126 let receipt = receipt_with_transaction_hash(RECEIPT_SUCCESS, expected_hash);
17127 let restart_state = with_finalized_identity(
17128 execution_rpc_state()
17129 .with_response("eth_getTransactionReceipt", &receipt)
17130 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d41", &block)
17131 .with_call_response(ALLOWANCE_SELECTOR, CALL_ZERO),
17132 &block,
17133 &receipt,
17134 );
17135 let addr = start_mock_rpc_server(restart_state.clone()).await;
17136 let mut restarted = test_client(format!("http://{addr}"));
17137 restarted.cache.database = Some(database);
17138 restarted.payload_keys = payload_keys;
17139 restarted.signer = Some(Arc::new(
17140 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
17141 ));
17142
17143 let error = restarted
17144 .reconcile_unresolved_execution()
17145 .await
17146 .unwrap_err();
17147
17148 assert!(
17149 error
17150 .to_string()
17151 .contains("does not equal the requested amount"),
17152 "was: {error}"
17153 );
17154 let in_flight = awaiting_in_flight(&restarted);
17155 assert_eq!(in_flight.nonce, 7);
17156 assert_eq!(in_flight.purpose, TransactionPurpose::Approve);
17157 assert!(
17158 restart_state
17159 .recorded_requests()
17160 .iter()
17161 .all(|request| request["method"] != "eth_sendRawTransaction")
17162 );
17163 assert_eq!(
17164 execution_intent_markers(&admin_pool, &schema).await,
17165 vec![("approve".into(), "dropped".into(), false, true)]
17166 );
17167
17168 let error = later_reconnect(restarted, format!("http://{addr}")).await;
17169 assert!(
17170 error
17171 .to_string()
17172 .contains("does not equal the requested amount"),
17173 "was: {error}"
17174 );
17175 assert_eq!(
17176 execution_intent_markers(&admin_pool, &schema).await,
17177 vec![("approve".into(), "dropped".into(), false, true)]
17178 );
17179
17180 drop_execution_schema(&admin_pool, &schema).await;
17181 }
17182
17183 #[tokio::test]
17184 async fn disappearing_unfinalized_receipt_drops_without_committing_inclusion() {
17185 let state = execution_rpc_state()
17186 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17187 .with_response("eth_estimateGas", ESTIMATE_GAS)
17188 .with_response("eth_call", CALL_BALANCE)
17189 .with_response_sequence(
17190 "eth_getTransactionReceipt",
17191 &[
17192 RECEIPT_SUCCESS,
17193 RECEIPT_SUCCESS,
17194 RECEIPT_SUCCESS,
17195 RECEIPT_NULL,
17196 RECEIPT_NULL,
17197 RECEIPT_NULL,
17198 ],
17199 )
17200 .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17201 .with_send_raw_transaction_echo();
17202 let Some((admin_pool, schema, mut client, _)) =
17203 execution_client_with_database("execution_receipt_disappeared_test", state).await
17204 else {
17205 return;
17206 };
17207 client.transaction_limits.receipt_timeout_secs = 2;
17208
17209 let error = client
17210 .wrap(U256::from(1_000_000_000_000_000_u64))
17211 .await
17212 .unwrap_err();
17213 let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17214 "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17215 )))
17216 .fetch_all(&admin_pool)
17217 .await
17218 .unwrap();
17219
17220 assert!(
17221 error.to_string().contains("Timed out awaiting finality"),
17222 "was: {error}"
17223 );
17224 assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17225 assert!(client.in_flight.lock().is_some());
17226
17227 drop_execution_schema(&admin_pool, &schema).await;
17228 }
17229
17230 #[tokio::test]
17231 async fn changed_unfinalized_block_drops_without_committing_inclusion() {
17232 let changed_block = serde_json::json!({
17233 "jsonrpc": "2.0",
17234 "id": 1,
17235 "result": {
17236 "number": "0x1cf0d41",
17237 "hash": "0x4444444444444444444444444444444444444444444444444444444444444444",
17238 "parentHash": FIXTURE_BLOCK_HASH,
17239 "timestamp": "0x69044a21",
17240 "baseFeePerGas": "0x5f5e100",
17241 "transactions": []
17242 }
17243 })
17244 .to_string();
17245 let state = execution_rpc_state()
17246 .with_response("eth_getTransactionCount", TRANSACTION_COUNT)
17247 .with_response("eth_estimateGas", ESTIMATE_GAS)
17248 .with_response("eth_call", CALL_BALANCE)
17249 .with_response_sequence(
17250 "eth_getTransactionReceipt",
17251 &[
17252 RECEIPT_SUCCESS,
17253 RECEIPT_SUCCESS,
17254 RECEIPT_SUCCESS,
17255 RECEIPT_SUCCESS,
17256 RECEIPT_SUCCESS,
17257 RECEIPT_SUCCESS,
17258 ],
17259 )
17260 .with_parameter_response_sequence(
17261 "eth_getBlockByNumber",
17262 "0x1cf0d41",
17263 &[
17264 BLOCK_CANONICAL,
17265 BLOCK_CANONICAL,
17266 BLOCK_CANONICAL,
17267 &changed_block,
17268 &changed_block,
17269 &changed_block,
17270 ],
17271 )
17272 .with_parameter_response("eth_getBlockByNumber", "finalized", BLOCK_BY_NUMBER)
17273 .with_send_raw_transaction_echo();
17274 let Some((admin_pool, schema, mut client, _)) =
17275 execution_client_with_database("execution_reorg_test", state).await
17276 else {
17277 return;
17278 };
17279 client.transaction_limits.receipt_timeout_secs = 2;
17280
17281 let error = client
17282 .wrap(U256::from(1_000_000_000_000_000_u64))
17283 .await
17284 .unwrap_err();
17285 let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17286 "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17287 )))
17288 .fetch_all(&admin_pool)
17289 .await
17290 .unwrap();
17291
17292 assert!(
17293 error.to_string().contains("Timed out awaiting finality"),
17294 "was: {error}"
17295 );
17296 assert_eq!(transitions, ["prepared", "signed", "broadcast", "dropped"]);
17297 assert!(client.in_flight.lock().is_some());
17298
17299 drop_execution_schema(&admin_pool, &schema).await;
17300 }
17301
17302 #[tokio::test]
17303 async fn pre_sign_pending_nonce_drift_blocks_wrap_before_signature() {
17304 let state = execution_rpc_state()
17305 .with_response_sequence(
17306 "eth_getTransactionCount",
17307 &[
17308 TRANSACTION_COUNT,
17309 TRANSACTION_COUNT,
17310 TRANSACTION_COUNT,
17311 TRANSACTION_COUNT_NEXT,
17312 TRANSACTION_COUNT_NEXT,
17313 TRANSACTION_COUNT_NEXT,
17314 ],
17315 )
17316 .with_response("eth_estimateGas", ESTIMATE_GAS)
17317 .with_call_response(BALANCE_OF_SELECTOR, CALL_BALANCE)
17318 .with_send_raw_transaction_echo();
17319 let Some((admin_pool, schema, mut client, state)) =
17320 execution_client_with_database("pre_sign_pending_nonce_drift", state).await
17321 else {
17322 return;
17323 };
17324
17325 let error = client
17326 .wrap(U256::from(1_000_000_000_000_000_u64))
17327 .await
17328 .unwrap_err();
17329 let signed_count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17330 "SELECT COUNT(*) FROM {schema}.execution_transaction_hash"
17331 )))
17332 .fetch_one(&admin_pool)
17333 .await
17334 .unwrap();
17335
17336 assert!(
17337 error
17338 .to_string()
17339 .contains("Pending nonce does not match the verified canonical nonce"),
17340 "was: {error}"
17341 );
17342 assert_eq!(signed_count, 0);
17343 assert!(
17344 state
17345 .recorded_requests()
17346 .iter()
17347 .all(|request| request["method"] != "eth_sendRawTransaction")
17348 );
17349 assert!(client.in_flight.lock().is_none());
17350
17351 drop_execution_schema(&admin_pool, &schema).await;
17352 }
17353
17354 #[tokio::test]
17355 async fn unknown_same_nonce_swap_replacement_emits_no_rejection() {
17356 let replacement_hash = B256::from([0x44; 32]);
17357 let replacement_block = replacement_head_block(replacement_hash);
17358 let state = execution_rpc_state()
17359 .with_parameter_response("eth_getBlockByNumber", "0x1cf0d40", &replacement_block)
17360 .with_send_raw_transaction_echo();
17361 let Some((admin_pool, schema, mut client, _, _)) =
17362 swap_client_with_database("unknown_same_nonce_swap_replacement", state).await
17363 else {
17364 return;
17365 };
17366 let mut receiver = start_with_events(&mut client);
17367 let database = client.cache.database.as_ref().unwrap();
17368 let (intent, original_hash, original_payload) =
17369 persist_test_swap_broadcast(database, client.payload_keys.as_deref()).await;
17370 let authenticated_payloads = HashMap::from([(original_hash, original_payload)]);
17371 let head = VerifiedBlockHeader {
17372 number: FIXTURE_BLOCK,
17373 hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17374 parent_hash: B256::from_str(
17375 "0x0000000000000000000000000000000000000000000000000000000000000001",
17376 )
17377 .unwrap(),
17378 timestamp: FIXTURE_BLOCK_TIMESTAMP,
17379 base_fee_per_gas: Some(100_000_000),
17380 };
17381
17382 let error = client
17383 .transaction_executor()
17384 .unwrap()
17385 .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17386 .await
17387 .unwrap_err();
17388 let events = collect_order_events(&mut receiver);
17389 let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17390 "SELECT status, active FROM {schema}.execution_intent"
17391 )))
17392 .fetch_one(&admin_pool)
17393 .await
17394 .unwrap();
17395
17396 assert!(
17397 error
17398 .to_string()
17399 .contains("has no authenticated retained payload"),
17400 "was: {error}"
17401 );
17402 assert!(events.is_empty(), "was: {events:?}");
17403 assert_eq!(status, "broadcast");
17404 assert!(active);
17405
17406 drop_execution_schema(&admin_pool, &schema).await;
17407 }
17408
17409 #[tokio::test]
17410 async fn replacement_scan_rejects_rpc_fields_that_conflict_with_the_payload() {
17411 let expected_hash = expected_wrap_tx_hash(U256::from(1_u64)).await;
17412 let mut replacement_block: serde_json::Value =
17413 serde_json::from_str(&replacement_head_block(expected_hash)).unwrap();
17414 replacement_block["result"]["transactions"][1]["value"] = serde_json::json!("0x2");
17415 let replacement_block = replacement_block.to_string();
17416 let state = execution_rpc_state().with_parameter_response(
17417 "eth_getBlockByNumber",
17418 "0x1cf0d40",
17419 &replacement_block,
17420 );
17421 let Some((admin_pool, schema, client, _)) =
17422 execution_client_with_database("replacement_scan_payload_mismatch", state).await
17423 else {
17424 return;
17425 };
17426 let database = client.cache.database.as_ref().unwrap();
17427 let (intent, persisted_hash, persisted_payload) =
17428 persist_test_wrap_broadcast(database, client.payload_keys.as_deref()).await;
17429 assert_eq!(persisted_hash, expected_hash);
17430 let authenticated_payloads = HashMap::from([(persisted_hash, persisted_payload)]);
17431 let head = VerifiedBlockHeader {
17432 number: FIXTURE_BLOCK,
17433 hash: B256::from_str(FIXTURE_BLOCK_HASH).unwrap(),
17434 parent_hash: B256::from_str(
17435 "0x0000000000000000000000000000000000000000000000000000000000000001",
17436 )
17437 .unwrap(),
17438 timestamp: FIXTURE_BLOCK_TIMESTAMP,
17439 base_fee_per_gas: Some(100_000_000),
17440 };
17441
17442 let error = client
17443 .transaction_executor()
17444 .unwrap()
17445 .scan_canonical_replacement(&intent, 7, head, &authenticated_payloads)
17446 .await
17447 .unwrap_err();
17448 let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17449 "SELECT status, active FROM {schema}.execution_intent"
17450 )))
17451 .fetch_one(&admin_pool)
17452 .await
17453 .unwrap();
17454
17455 assert!(
17456 error
17457 .to_string()
17458 .contains("failed authenticated payload validation"),
17459 "was: {error}"
17460 );
17461 assert_eq!(status, "broadcast");
17462 assert!(active);
17463
17464 drop_execution_schema(&admin_pool, &schema).await;
17465 }
17466
17467 #[tokio::test]
17468 async fn execution_transaction_constraints_reject_conflicting_identity() {
17469 const TRANSACTION_HASH: &str = "0xduplicate-transaction-hash";
17470 const OTHER_WALLET: &str = "0x0000000000000000000000000000000000000001";
17471 let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17472 "execution_duplicate_record_test",
17473 ready_rpc_state(),
17474 )
17475 .await
17476 else {
17477 return;
17478 };
17479 let database = client.cache.database.as_ref().unwrap();
17480 let operator = database
17481 .reserve_execution_intent(&ExecutionIntentInsert {
17482 chain_id: 42161,
17483 wallet_address: WALLET.to_string(),
17484 purpose: "wrap".to_string(),
17485 client_order_id: None,
17486 trader_id: None,
17487 strategy_id: None,
17488 account_id: None,
17489 instrument_id: None,
17490 pool_address: None,
17491 transaction_to: WETH_ADDRESS.to_string(),
17492 transaction_input: "0xd0e30db0".to_string(),
17493 transaction_value: "1".to_string(),
17494 amount_in: None,
17495 created_block: FIXTURE_BLOCK,
17496 })
17497 .await
17498 .unwrap();
17499 database
17500 .assign_execution_intent_nonce(operator.id, 7)
17501 .await
17502 .unwrap();
17503 database
17504 .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17505 .await
17506 .unwrap();
17507 database
17508 .add_execution_transaction_hash(operator.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17509 .await
17510 .unwrap();
17511
17512 let signer_conflict = database
17513 .reserve_execution_intent(&ExecutionIntentInsert {
17514 chain_id: 42161,
17515 wallet_address: WALLET.to_string(),
17516 purpose: "approve".to_string(),
17517 client_order_id: None,
17518 trader_id: None,
17519 strategy_id: None,
17520 account_id: None,
17521 instrument_id: None,
17522 pool_address: None,
17523 transaction_to: ROUTER_ADDRESS.to_string(),
17524 transaction_input: "0x01".to_string(),
17525 transaction_value: "0".to_string(),
17526 amount_in: None,
17527 created_block: FIXTURE_BLOCK,
17528 })
17529 .await
17530 .unwrap_err();
17531 let other = database
17532 .reserve_execution_intent(&ExecutionIntentInsert {
17533 chain_id: 42161,
17534 wallet_address: OTHER_WALLET.to_string(),
17535 purpose: "approve".to_string(),
17536 client_order_id: None,
17537 trader_id: None,
17538 strategy_id: None,
17539 account_id: None,
17540 instrument_id: None,
17541 pool_address: None,
17542 transaction_to: ROUTER_ADDRESS.to_string(),
17543 transaction_input: "0x01".to_string(),
17544 transaction_value: "0".to_string(),
17545 amount_in: None,
17546 created_block: FIXTURE_BLOCK,
17547 })
17548 .await
17549 .unwrap();
17550 database
17551 .assign_execution_intent_nonce(other.id, 7)
17552 .await
17553 .unwrap();
17554 let hash_conflict = database
17555 .add_execution_transaction_hash(other.id, 42161, TRANSACTION_HASH, &[4, 5, 6])
17556 .await
17557 .unwrap_err();
17558
17559 let record = database
17560 .get_execution_transaction(42161, TRANSACTION_HASH)
17561 .await
17562 .unwrap()
17563 .unwrap();
17564 let count: i64 = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17565 "SELECT COUNT(*) FROM {schema}.execution_intent"
17566 )))
17567 .fetch_one(&admin_pool)
17568 .await
17569 .unwrap();
17570
17571 assert_eq!(record.wallet_address.as_deref(), Some(WALLET));
17572 assert_eq!(record.nonce, 7);
17573 assert_eq!(record.transaction_hash, TRANSACTION_HASH);
17574 assert_eq!(record.purpose, "wrap");
17575 assert_eq!(record.status, "signed");
17576 assert_eq!(record.client_order_id, None);
17577 assert!(
17578 hash_conflict
17579 .to_string()
17580 .contains("conflicts with its persisted identity"),
17581 "was: {hash_conflict}"
17582 );
17583 assert_eq!(
17584 signer_conflict.to_string(),
17585 "Execution intent reservation failed before commit"
17586 );
17587 assert!(
17588 signer_conflict.chain().any(|cause| cause
17589 .to_string()
17590 .contains("execution_intent_active_signer_key")),
17591 "was: {signer_conflict:#}"
17592 );
17593 assert_eq!(count, 2);
17594
17595 drop_execution_schema(&admin_pool, &schema).await;
17596 }
17597
17598 #[tokio::test]
17599 async fn execution_status_transitions_are_idempotent() {
17600 const TRANSACTION_HASH: &str =
17601 "0x5555555555555555555555555555555555555555555555555555555555555555";
17602 let Some((admin_pool, schema, client, _)) = execution_client_with_unprotected_database(
17603 "execution_transition_test",
17604 ready_rpc_state(),
17605 )
17606 .await
17607 else {
17608 return;
17609 };
17610 let database = client.cache.database.as_ref().unwrap();
17611 let intent = database
17612 .reserve_execution_intent(&ExecutionIntentInsert {
17613 chain_id: 42161,
17614 wallet_address: WALLET.to_string(),
17615 purpose: "wrap".to_string(),
17616 client_order_id: None,
17617 trader_id: None,
17618 strategy_id: None,
17619 account_id: None,
17620 instrument_id: None,
17621 pool_address: None,
17622 transaction_to: WETH_ADDRESS.to_string(),
17623 transaction_input: "0xd0e30db0".to_string(),
17624 transaction_value: "1".to_string(),
17625 amount_in: None,
17626 created_block: FIXTURE_BLOCK,
17627 })
17628 .await
17629 .unwrap();
17630 database
17631 .assign_execution_intent_nonce(intent.id, 7)
17632 .await
17633 .unwrap();
17634 database
17635 .add_execution_transaction_hash(intent.id, 42161, TRANSACTION_HASH, &[1, 2, 3])
17636 .await
17637 .unwrap();
17638
17639 for _ in 0..2 {
17640 database
17641 .record_execution_status(
17642 intent.id,
17643 TRANSACTION_HASH,
17644 TransactionStatus::Broadcast,
17645 None,
17646 None,
17647 None,
17648 None,
17649 None,
17650 )
17651 .await
17652 .unwrap();
17653 }
17654
17655 for status in [TransactionStatus::Included, TransactionStatus::Included] {
17656 database
17657 .record_execution_status(
17658 intent.id,
17659 TRANSACTION_HASH,
17660 status,
17661 Some(FIXTURE_BLOCK + 1),
17662 Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17663 Some(true),
17664 Some(50_112),
17665 Some("100000000"),
17666 )
17667 .await
17668 .unwrap();
17669 }
17670
17671 for _ in 0..2 {
17672 database
17673 .record_execution_status(
17674 intent.id,
17675 TRANSACTION_HASH,
17676 TransactionStatus::Finalized,
17677 Some(FIXTURE_BLOCK + 1),
17678 Some("0x2222222222222222222222222222222222222222222222222222222222222222"),
17679 Some(true),
17680 Some(50_112),
17681 Some("100000000"),
17682 )
17683 .await
17684 .unwrap();
17685 }
17686
17687 let transitions: Vec<String> = sqlx::query_scalar(sqlx::AssertSqlSafe(format!(
17688 "SELECT to_status FROM {schema}.execution_transaction_transition ORDER BY id"
17689 )))
17690 .fetch_all(&admin_pool)
17691 .await
17692 .unwrap();
17693 let (status, active): (String, bool) = sqlx::query_as(sqlx::AssertSqlSafe(format!(
17694 "SELECT status, active FROM {schema}.execution_intent"
17695 )))
17696 .fetch_one(&admin_pool)
17697 .await
17698 .unwrap();
17699 let append_only_error = sqlx::query(sqlx::AssertSqlSafe(format!(
17700 "DELETE FROM {schema}.execution_transaction_transition WHERE intent_id = {}",
17701 intent.id
17702 )))
17703 .execute(&admin_pool)
17704 .await
17705 .unwrap_err();
17706
17707 assert_eq!(
17708 transitions,
17709 ["prepared", "signed", "broadcast", "included", "finalized"]
17710 );
17711 assert_eq!(status, "finalized");
17712 assert!(active);
17713 assert!(
17714 append_only_error
17715 .to_string()
17716 .contains("Execution transitions are append-only"),
17717 "was: {append_only_error}"
17718 );
17719
17720 drop_execution_schema(&admin_pool, &schema).await;
17721 }
17722
17723 #[tokio::test]
17724 async fn wrap_then_approve_persists_records_and_clears_in_flight() {
17725 let state = execution_rpc_state()
17726 .with_parameter_response_sequence(
17727 "eth_getBlockByNumber",
17728 "latest",
17729 &[
17730 BLOCK_BY_NUMBER,
17731 BLOCK_BY_NUMBER,
17732 BLOCK_BY_NUMBER,
17733 BLOCK_FINALIZED,
17734 BLOCK_FINALIZED,
17735 BLOCK_FINALIZED,
17736 ],
17737 )
17738 .with_response_sequence(
17739 "eth_call",
17740 &[
17741 CALL_BALANCE,
17742 CALL_BALANCE,
17743 CALL_BALANCE,
17744 CALL_BALANCE,
17745 CALL_BALANCE,
17746 CALL_BALANCE,
17747 CALL_BALANCE_AFTER_WRAP,
17748 CALL_BALANCE_AFTER_WRAP,
17749 CALL_BALANCE_AFTER_WRAP,
17750 CALL_BOOL_TRUE,
17751 CALL_BOOL_TRUE,
17752 CALL_BOOL_TRUE,
17753 ],
17754 )
17755 .with_call_response_sequence(
17756 ALLOWANCE_SELECTOR,
17757 &[
17758 CALL_ZERO,
17759 CALL_ZERO,
17760 CALL_ZERO,
17761 CALL_ALLOWANCE_MAX,
17762 CALL_ALLOWANCE_MAX,
17763 CALL_ALLOWANCE_MAX,
17764 ],
17765 )
17766 .with_response_sequence(
17767 "eth_getTransactionCount",
17768 &[
17769 TRANSACTION_COUNT,
17770 TRANSACTION_COUNT,
17771 TRANSACTION_COUNT,
17772 TRANSACTION_COUNT,
17773 TRANSACTION_COUNT,
17774 TRANSACTION_COUNT,
17775 TRANSACTION_COUNT_NEXT,
17776 TRANSACTION_COUNT_NEXT,
17777 TRANSACTION_COUNT_NEXT,
17778 TRANSACTION_COUNT_NEXT,
17779 TRANSACTION_COUNT_NEXT,
17780 TRANSACTION_COUNT_NEXT,
17781 ],
17782 )
17783 .with_response("eth_estimateGas", ESTIMATE_GAS)
17784 .with_response_sequence(
17785 "eth_getTransactionReceipt",
17786 &[
17787 RECEIPT_SUCCESS,
17788 RECEIPT_SUCCESS,
17789 RECEIPT_SUCCESS,
17790 RECEIPT_SUCCESS,
17791 RECEIPT_SUCCESS,
17792 RECEIPT_SUCCESS,
17793 ],
17794 )
17795 .with_send_raw_transaction_echo();
17796 let Some((admin_pool, schema, mut client, state)) =
17797 execution_client_with_database("execution_client_test", state).await
17798 else {
17799 return;
17800 };
17801 client.config.unlimited_approval = true;
17802 client.transaction_limits.receipt_timeout_secs = 3;
17803
17804 let wrap_hash = client
17805 .wrap(U256::from(1_000_000_000_000_000u64))
17806 .await
17807 .unwrap();
17808
17809 let record = client
17810 .cache
17811 .get_execution_transaction(42161, &wrap_hash.to_string())
17812 .await
17813 .unwrap()
17814 .unwrap();
17815 assert_eq!(record.nonce, 7);
17816 assert_eq!(record.purpose, "wrap");
17817 assert_eq!(record.status, "finalized");
17818 assert_eq!(
17819 execution_intent_markers(&admin_pool, &schema).await,
17820 vec![("wrap".into(), "finalized".into(), true, false)]
17821 );
17822
17823 let approve_hash = client
17824 .approve(WETH_ADDRESS, U256::from(1_000u64), ROUTER_ADDRESS)
17825 .await
17826 .unwrap();
17827
17828 let record = client
17829 .cache
17830 .get_execution_transaction(42161, &approve_hash.to_string())
17831 .await
17832 .unwrap()
17833 .unwrap();
17834 assert_eq!(record.nonce, 8);
17835 assert_eq!(record.purpose, "approve");
17836 assert_eq!(record.status, "finalized");
17837 assert_eq!(
17838 execution_intent_markers(&admin_pool, &schema).await,
17839 vec![
17840 ("wrap".into(), "finalized".into(), true, false),
17841 ("approve".into(), "finalized".into(), true, false),
17842 ]
17843 );
17844
17845 let requests = state.recorded_requests();
17846 let broadcasts: Vec<_> = requests
17847 .iter()
17848 .filter(|request| request["method"] == "eth_sendRawTransaction")
17849 .collect();
17850 assert_eq!(broadcasts.len(), 2);
17851 for broadcast in &broadcasts {
17852 let payload = broadcast["params"][0].as_str().unwrap();
17853 assert!(payload.starts_with("0x02"), "was: {payload}");
17854 }
17855 let broadcast_indexes = requests
17856 .iter()
17857 .enumerate()
17858 .filter_map(|(index, request)| {
17859 (request["method"] == "eth_sendRawTransaction").then_some(index)
17860 })
17861 .collect::<Vec<_>>();
17862
17863 for (index, expected_block) in broadcast_indexes
17864 .into_iter()
17865 .zip([FIXTURE_BLOCK_PARAM, "0x1cf0d42"])
17866 {
17867 let nonce_fence = &requests[index - 6..index];
17868 assert!(nonce_fence[..3].iter().all(|request| {
17869 request["method"] == "eth_getTransactionCount"
17870 && request["params"][1] == expected_block
17871 }));
17872 assert!(nonce_fence[3..].iter().all(|request| {
17873 request["method"] == "eth_getTransactionCount" && request["params"][1] == "pending"
17874 }));
17875 }
17876 let receipt_polls = requests
17877 .iter()
17878 .filter(|request| request["method"] == "eth_getTransactionReceipt")
17879 .count();
17880 assert_eq!(receipt_polls, 6);
17881 let allowance_calls: Vec<_> = requests
17882 .iter()
17883 .filter(|request| {
17884 request["method"] == "eth_call"
17885 && request["params"][0]["data"]
17886 .as_str()
17887 .is_some_and(|data| data.starts_with(ALLOWANCE_SELECTOR))
17888 })
17889 .collect();
17890 assert_eq!(allowance_calls.len(), 6);
17891 assert!(
17892 allowance_calls[..3]
17893 .iter()
17894 .all(|request| request["params"][1] == "0x1cf0d42")
17895 );
17896 assert!(
17897 allowance_calls[3..]
17898 .iter()
17899 .all(|request| request["params"][1] == "0x1cf0d41")
17900 );
17901
17902 let estimates: Vec<_> = requests
17904 .iter()
17905 .filter(|request| request["method"] == "eth_estimateGas")
17906 .collect();
17907 assert_eq!(estimates.len(), 6);
17908 assert!(estimates[..3].iter().all(|request| {
17909 request["params"].as_array().unwrap().len() == 2
17910 && request["params"][1] == FIXTURE_BLOCK_PARAM
17911 }));
17912 assert!(estimates[3..].iter().all(|request| {
17913 request["params"].as_array().unwrap().len() == 2 && request["params"][1] == "0x1cf0d42"
17914 }));
17915 let approve_data = estimates[3]["params"][0]["data"].as_str().unwrap();
17916 assert!(
17917 approve_data.starts_with("0x095ea7b3"),
17918 "was: {approve_data}"
17919 );
17920 assert!(
17921 approve_data.ends_with(&"f".repeat(64)),
17922 "was: {approve_data}"
17923 );
17924
17925 for selector in [FACTORY_SELECTOR, WETH9_SELECTOR] {
17926 let calls: Vec<_> = requests
17927 .iter()
17928 .filter(|request| {
17929 request["method"] == "eth_call"
17930 && request["params"][0]["data"]
17931 .as_str()
17932 .is_some_and(|data| data.starts_with(selector))
17933 })
17934 .collect();
17935 assert!(!calls.is_empty(), "selector {selector}");
17936 assert!(
17937 calls.iter().all(|request| request["params"][1]
17938 .as_str()
17939 .is_some_and(|block| block.starts_with("0x"))),
17940 "selector {selector}: {calls:?}"
17941 );
17942 }
17943 let latest_blocks = requests
17944 .iter()
17945 .filter(|request| {
17946 request["method"] == "eth_getBlockByNumber"
17947 && request["params"] == serde_json::json!(["latest", false])
17948 })
17949 .count();
17950 assert_eq!(latest_blocks, 6);
17951
17952 drop_execution_schema(&admin_pool, &schema).await;
17953 }
17954
17955 #[tokio::test]
17956 async fn reverted_receipt_marks_record_reverted_and_errors() {
17957 let state = signing_rpc_state()
17958 .with_response("eth_getTransactionReceipt", RECEIPT_REVERTED)
17959 .with_send_raw_transaction_echo();
17960 let Some((admin_pool, schema, mut client, _)) =
17961 execution_client_with_database("execution_reverted_test", state).await
17962 else {
17963 return;
17964 };
17965
17966 let error = client
17967 .wrap(U256::from(1_000_000_000_000_000u64))
17968 .await
17969 .unwrap_err();
17970
17971 assert!(
17972 error.to_string().contains("reverted on-chain"),
17973 "was: {error}"
17974 );
17975 assert!(client.in_flight.lock().is_none());
17976
17977 let expected_hash = expected_wrap_tx_hash(U256::from(1_000_000_000_000_000u64)).await;
17978
17979 let record = client
17980 .cache
17981 .get_execution_transaction(42161, &expected_hash.to_string())
17982 .await
17983 .unwrap()
17984 .unwrap();
17985 assert_eq!(record.status, "reverted");
17986 assert_eq!(
17987 execution_intent_markers(&admin_pool, &schema).await,
17988 vec![("wrap".into(), "reverted".into(), true, false)]
17989 );
17990
17991 drop_execution_schema(&admin_pool, &schema).await;
17992 }
17993
17994 fn market_sell_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
17995 OrderTestBuilder::new(OrderType::Market)
17996 .trader_id(TraderId::from("TRADER-001"))
17997 .strategy_id(StrategyId::from("S-001"))
17998 .instrument_id(instrument_id)
17999 .client_order_id(ClientOrderId::from(client_order_id))
18000 .side(OrderSide::Sell)
18001 .quantity(Quantity::from("0.001"))
18002 .build()
18003 }
18004
18005 fn market_buy_order_with_id(instrument_id: InstrumentId, client_order_id: &str) -> OrderAny {
18006 OrderTestBuilder::new(OrderType::Market)
18007 .trader_id(TraderId::from("TRADER-001"))
18008 .strategy_id(StrategyId::from("S-001"))
18009 .instrument_id(instrument_id)
18010 .client_order_id(ClientOrderId::from(client_order_id))
18011 .side(OrderSide::Buy)
18012 .quantity(Quantity::from("0.001"))
18013 .build()
18014 }
18015
18016 fn submit_order_list_cmd(orders: &[OrderAny]) -> SubmitOrderList {
18017 let order_list = OrderList::new(
18018 OrderListId::from("OL-001"),
18019 orders[0].instrument_id(),
18020 orders[0].strategy_id(),
18021 orders.iter().map(|order| order.client_order_id()).collect(),
18022 UnixNanos::default(),
18023 );
18024 SubmitOrderList::new(
18025 TraderId::from("TRADER-001"),
18026 Some(ClientId::from("BLOCKCHAIN-001")),
18027 orders[0].strategy_id(),
18028 order_list,
18029 orders
18030 .iter()
18031 .map(|order| order.init_event().clone())
18032 .collect(),
18033 None,
18034 None,
18035 None,
18036 UUID4::new(),
18037 UnixNanos::default(),
18038 None,
18039 )
18040 }
18041
18042 fn modify_order_cmd(
18043 instrument_id: InstrumentId,
18044 client_order_id: ClientOrderId,
18045 ) -> ModifyOrder {
18046 ModifyOrder::new(
18047 TraderId::from("TRADER-001"),
18048 Some(ClientId::from("BLOCKCHAIN-001")),
18049 StrategyId::from("S-001"),
18050 instrument_id,
18051 client_order_id,
18052 None,
18053 Some(Quantity::from("0.002")),
18054 None,
18055 None,
18056 UUID4::new(),
18057 UnixNanos::default(),
18058 None,
18059 None,
18060 )
18061 }
18062
18063 fn cancel_order_cmd(
18064 instrument_id: InstrumentId,
18065 client_order_id: ClientOrderId,
18066 ) -> CancelOrder {
18067 CancelOrder::new(
18068 TraderId::from("TRADER-001"),
18069 Some(ClientId::from("BLOCKCHAIN-001")),
18070 StrategyId::from("S-001"),
18071 instrument_id,
18072 client_order_id,
18073 None,
18074 UUID4::new(),
18075 UnixNanos::default(),
18076 None,
18077 None,
18078 )
18079 }
18080
18081 fn cancel_all_orders_cmd(instrument_id: InstrumentId) -> CancelAllOrders {
18082 CancelAllOrders::new(
18083 TraderId::from("TRADER-001"),
18084 Some(ClientId::from("BLOCKCHAIN-001")),
18085 StrategyId::from("S-001"),
18086 instrument_id,
18087 Some(OrderSide::Sell),
18088 UUID4::new(),
18089 UnixNanos::default(),
18090 None,
18091 None,
18092 )
18093 }
18094
18095 fn batch_cancel_orders_cmd(cancels: Vec<CancelOrder>) -> BatchCancelOrders {
18096 BatchCancelOrders::new(
18097 TraderId::from("TRADER-001"),
18098 Some(ClientId::from("BLOCKCHAIN-001")),
18099 StrategyId::from("S-001"),
18100 cancels[0].instrument_id,
18101 cancels,
18102 UUID4::new(),
18103 UnixNanos::default(),
18104 None,
18105 None,
18106 )
18107 }
18108
18109 fn query_order_cmd(instrument_id: InstrumentId, client_order_id: ClientOrderId) -> QueryOrder {
18110 QueryOrder::new(
18111 TraderId::from("TRADER-001"),
18112 Some(ClientId::from("BLOCKCHAIN-001")),
18113 StrategyId::from("S-001"),
18114 instrument_id,
18115 client_order_id,
18116 None,
18117 UUID4::new(),
18118 UnixNanos::default(),
18119 None,
18120 None,
18121 )
18122 }
18123
18124 async fn unsupported_client_with_mock_rpc()
18125 -> (BlockchainExecutionClient, MockRpcState, Rc<RefCell<Cache>>) {
18126 let state = ready_rpc_state();
18127 let addr = start_mock_rpc_server(state.clone()).await;
18128 let (client, cache) = swap_client_with_cache(test_config(format!("http://{addr}")));
18129 (client, state, cache)
18130 }
18131
18132 #[tokio::test]
18133 async fn submit_order_denies_reduce_only_without_side_effects() {
18134 let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18135 let order = OrderTestBuilder::new(OrderType::Market)
18136 .trader_id(TraderId::from("TRADER-001"))
18137 .strategy_id(StrategyId::from("S-001"))
18138 .instrument_id(test_pool().instrument_id)
18139 .client_order_id(ClientOrderId::from("O-REDUCE-ONLY"))
18140 .side(OrderSide::Sell)
18141 .quantity(Quantity::from("0.001"))
18142 .reduce_only(true)
18143 .build();
18144 cache
18145 .borrow_mut()
18146 .add_order(order.clone(), None, None, false)
18147 .unwrap();
18148 let mut receiver = start_with_events(&mut client);
18149
18150 client.submit_order(submit_order_cmd(&order)).unwrap();
18151
18152 let events = collect_order_events(&mut receiver);
18153 assert_eq!(events.len(), 1, "was: {events:?}");
18154 let OrderEventAny::Denied(denied) = &events[0] else {
18155 panic!("expected OrderDenied, was {:?}", events[0]);
18156 };
18157 assert_eq!(denied.client_order_id, order.client_order_id());
18158 assert_eq!(denied.reason, "UNSUPPORTED_REDUCE_ONLY");
18159 assert!(state.recorded_requests().is_empty());
18160 assert!(client.in_flight.lock().is_none());
18161 }
18162
18163 #[tokio::test]
18164 async fn submit_order_list_denies_every_order_without_side_effects() {
18165 let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18166 let pool = test_pool();
18167 let first = test_market_sell_order(pool.instrument_id);
18168 let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18169 cache
18170 .borrow_mut()
18171 .add_order(second.clone(), None, None, false)
18172 .unwrap();
18173 let orders = [first.clone(), second.clone()];
18174 let mut receiver = start_with_events(&mut client);
18175
18176 client
18177 .submit_order_list(submit_order_list_cmd(&orders))
18178 .unwrap();
18179
18180 let events = collect_order_events(&mut receiver);
18181 let mut denied_ids = Vec::new();
18182
18183 for event in &events {
18184 let OrderEventAny::Denied(denied) = event else {
18185 panic!("expected OrderDenied, was {event:?}");
18186 };
18187 assert_eq!(denied.reason, ORDER_LIST_UNSUPPORTED);
18188 denied_ids.push(denied.client_order_id);
18189 }
18190 denied_ids.sort();
18191 assert_eq!(
18192 denied_ids,
18193 [first.client_order_id(), second.client_order_id()]
18194 );
18195 assert!(state.recorded_requests().is_empty());
18196 assert!(client.in_flight.lock().is_none());
18197
18198 for order in &orders {
18199 let cache_ref = cache.borrow();
18200 let cached = cache_ref.order(&order.client_order_id()).unwrap();
18201 assert_eq!(cached.status(), OrderStatus::Initialized);
18202 }
18203 }
18204
18205 #[tokio::test]
18206 async fn modify_order_rejects_without_side_effects() {
18207 let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18208 let order = test_market_sell_order(test_pool().instrument_id);
18209 let mut receiver = start_with_events(&mut client);
18210
18211 client
18212 .modify_order(modify_order_cmd(
18213 order.instrument_id(),
18214 order.client_order_id(),
18215 ))
18216 .unwrap();
18217
18218 let events = collect_order_events(&mut receiver);
18219 assert_eq!(events.len(), 1, "was: {events:?}");
18220 let OrderEventAny::ModifyRejected(rejected) = &events[0] else {
18221 panic!("expected OrderModifyRejected, was {:?}", events[0]);
18222 };
18223 assert_eq!(rejected.client_order_id, order.client_order_id());
18224 assert_eq!(rejected.reason, ORDER_MODIFY_UNSUPPORTED);
18225 assert!(state.recorded_requests().is_empty());
18226 assert!(client.in_flight.lock().is_none());
18227 let cache_ref = cache.borrow();
18228 let cached = cache_ref.order(&order.client_order_id()).unwrap();
18229 assert_eq!(cached.status(), OrderStatus::Initialized);
18230 }
18231
18232 #[tokio::test]
18233 async fn cancel_order_rejects_without_side_effects() {
18234 let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18235 let order = test_market_sell_order(test_pool().instrument_id);
18236 let mut receiver = start_with_events(&mut client);
18237
18238 client
18239 .cancel_order(cancel_order_cmd(
18240 order.instrument_id(),
18241 order.client_order_id(),
18242 ))
18243 .unwrap();
18244
18245 let events = collect_order_events(&mut receiver);
18246 assert_eq!(events.len(), 1, "was: {events:?}");
18247 let OrderEventAny::CancelRejected(rejected) = &events[0] else {
18248 panic!("expected OrderCancelRejected, was {:?}", events[0]);
18249 };
18250 assert_eq!(rejected.client_order_id, order.client_order_id());
18251 assert_eq!(rejected.reason, ORDER_CANCEL_UNSUPPORTED);
18252 assert!(state.recorded_requests().is_empty());
18253 assert!(client.in_flight.lock().is_none());
18254 let cache_ref = cache.borrow();
18255 let cached = cache_ref.order(&order.client_order_id()).unwrap();
18256 assert_eq!(cached.status(), OrderStatus::Initialized);
18257 }
18258
18259 #[tokio::test]
18260 async fn batch_cancel_orders_rejects_each_order_without_side_effects() {
18261 let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18262 let pool = test_pool();
18263 let first = test_market_sell_order(pool.instrument_id);
18264 let second = market_sell_order_with_id(pool.instrument_id, "O-SWAP-002");
18265 cache
18266 .borrow_mut()
18267 .add_order(second.clone(), None, None, false)
18268 .unwrap();
18269 let orders = [first.clone(), second.clone()];
18270 let cancels = orders
18271 .iter()
18272 .map(|order| cancel_order_cmd(order.instrument_id(), order.client_order_id()))
18273 .collect();
18274 let mut receiver = start_with_events(&mut client);
18275
18276 client
18277 .batch_cancel_orders(batch_cancel_orders_cmd(cancels))
18278 .unwrap();
18279
18280 let events = collect_order_events(&mut receiver);
18281 let mut rejected_ids = Vec::new();
18282
18283 for event in &events {
18284 let OrderEventAny::CancelRejected(rejected) = event else {
18285 panic!("expected OrderCancelRejected, was {event:?}");
18286 };
18287 assert_eq!(rejected.reason, ORDER_CANCEL_UNSUPPORTED);
18288 rejected_ids.push(rejected.client_order_id);
18289 }
18290 rejected_ids.sort();
18291 assert_eq!(
18292 rejected_ids,
18293 [first.client_order_id(), second.client_order_id()]
18294 );
18295 assert!(state.recorded_requests().is_empty());
18296 assert!(client.in_flight.lock().is_none());
18297 }
18298
18299 #[tokio::test]
18300 async fn cancel_all_and_query_order_log_unsupported_without_side_effects() {
18301 let (mut client, state, cache) = unsupported_client_with_mock_rpc().await;
18302 let order = test_market_sell_order(test_pool().instrument_id);
18303 let mut receiver = start_with_events(&mut client);
18304
18305 client
18306 .cancel_all_orders(cancel_all_orders_cmd(order.instrument_id()))
18307 .unwrap();
18308 client
18309 .query_order(query_order_cmd(
18310 order.instrument_id(),
18311 order.client_order_id(),
18312 ))
18313 .unwrap();
18314
18315 let events = collect_order_events(&mut receiver);
18316 assert!(events.is_empty(), "was: {events:?}");
18317 assert!(state.recorded_requests().is_empty());
18318 assert!(client.in_flight.lock().is_none());
18319 let cache_ref = cache.borrow();
18320 let cached = cache_ref.order(&order.client_order_id()).unwrap();
18321 assert_eq!(cached.status(), OrderStatus::Initialized);
18322 }
18323
18324 #[tokio::test]
18325 async fn unsupported_commands_handle_unknown_orders_without_panic() {
18326 let (mut client, state, _) = unsupported_client_with_mock_rpc().await;
18327 let instrument_id = test_pool().instrument_id;
18328 let unknown = ClientOrderId::from("O-UNKNOWN");
18329 let mut receiver = start_with_events(&mut client);
18330
18331 client
18332 .modify_order(modify_order_cmd(instrument_id, unknown))
18333 .unwrap();
18334 client
18335 .cancel_order(cancel_order_cmd(instrument_id, unknown))
18336 .unwrap();
18337 client
18338 .batch_cancel_orders(batch_cancel_orders_cmd(vec![cancel_order_cmd(
18339 instrument_id,
18340 unknown,
18341 )]))
18342 .unwrap();
18343 client
18344 .query_order(query_order_cmd(instrument_id, unknown))
18345 .unwrap();
18346
18347 let events = collect_order_events(&mut receiver);
18348 assert!(events.is_empty(), "was: {events:?}");
18349 assert!(state.recorded_requests().is_empty());
18350 assert!(client.in_flight.lock().is_none());
18351 }
18352
18353 #[tokio::test]
18354 async fn report_generators_error_except_mass_status_without_side_effects() {
18355 let (client, state, _) = unsupported_client_with_mock_rpc().await;
18356
18357 let report = client
18358 .generate_order_status_report(&GenerateOrderStatusReport::new(
18359 UUID4::new(),
18360 UnixNanos::default(),
18361 None,
18362 None,
18363 None,
18364 None,
18365 None,
18366 ))
18367 .await
18368 .unwrap_err();
18369 assert_eq!(report.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18370
18371 let reports = client
18372 .generate_order_status_reports(&GenerateOrderStatusReports::new(
18373 UUID4::new(),
18374 UnixNanos::default(),
18375 false,
18376 None,
18377 None,
18378 None,
18379 None,
18380 None,
18381 ))
18382 .await
18383 .unwrap_err();
18384 assert_eq!(reports.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18385
18386 let fills = client
18387 .generate_fill_reports(GenerateFillReports::new(
18388 UUID4::new(),
18389 UnixNanos::default(),
18390 None,
18391 None,
18392 None,
18393 None,
18394 None,
18395 None,
18396 ))
18397 .await
18398 .unwrap_err();
18399 assert_eq!(fills.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18400
18401 let positions = client
18402 .generate_position_status_reports(&GeneratePositionStatusReports::new(
18403 UUID4::new(),
18404 UnixNanos::default(),
18405 None,
18406 None,
18407 None,
18408 None,
18409 None,
18410 ))
18411 .await
18412 .unwrap_err();
18413 assert_eq!(positions.to_string(), VENUE_EXECUTION_REPORTS_UNSUPPORTED);
18414
18415 let mass_status = client.generate_mass_status(None).await.unwrap();
18416 assert!(mass_status.is_none());
18417
18418 let mass_status = client.generate_mass_status(Some(60)).await.unwrap();
18419 assert!(mass_status.is_none());
18420
18421 assert!(state.recorded_requests().is_empty());
18422 assert!(client.in_flight.lock().is_none());
18423 }
18424
18425 async fn connect_test_postgres(
18426 test_name: &str,
18427 ) -> Option<(sqlx::PgPool, PostgresConnectOptions)> {
18428 let pg_config = get_postgres_connect_options(None, None, None, None, None);
18429 let admin_options: sqlx::postgres::PgConnectOptions = pg_config.clone().into();
18430 let admin_pool = match PgPoolOptions::new()
18431 .max_connections(1)
18432 .connect_with(admin_options)
18433 .await
18434 {
18435 Ok(pool) => pool,
18436 Err(e) => {
18437 eprintln!("Postgres unavailable; skipping {test_name} test: {e}");
18438 return None;
18439 }
18440 };
18441
18442 Some((admin_pool, pg_config))
18443 }
18444
18445 async fn execution_client_with_database(
18446 test_name: &str,
18447 state: MockRpcState,
18448 ) -> Option<(
18449 sqlx::PgPool,
18450 String,
18451 BlockchainExecutionClient,
18452 MockRpcState,
18453 )> {
18454 let (admin_pool, schema, mut client, state) =
18455 execution_client_with_unprotected_database(test_name, state).await?;
18456 protect_test_storage(&mut client, &schema).await;
18457 Some((admin_pool, schema, client, state))
18458 }
18459
18460 async fn execution_client_with_unprotected_database(
18461 test_name: &str,
18462 state: MockRpcState,
18463 ) -> Option<(
18464 sqlx::PgPool,
18465 String,
18466 BlockchainExecutionClient,
18467 MockRpcState,
18468 )> {
18469 let (admin_pool, pg_config) = connect_test_postgres(test_name).await?;
18470 let schema = format!("{test_name}_{}", std::process::id());
18471 setup_execution_schema(&admin_pool, &schema).await;
18472
18473 let db_options: sqlx::postgres::PgConnectOptions = pg_config.into();
18474 let db_options = db_options.options([("search_path", schema.clone())]);
18475 let database = connect_test_database(db_options).await.unwrap();
18476 let addr = start_mock_rpc_server(state.clone()).await;
18477 let mut client = test_client(format!("http://{addr}"));
18478 client.cache.database = Some(database);
18479 client
18481 .cache
18482 .ensure_execution_transaction_schema()
18483 .await
18484 .unwrap();
18485 initialize_test_verification_ledger(&client).await;
18486 client.signer = Some(Arc::new(
18487 PrivateKeySigner::from_str(TEST_PRIVATE_KEY).unwrap(),
18488 ));
18489 client.core.set_connected();
18490
18491 Some((admin_pool, schema, client, state))
18492 }
18493
18494 async fn install_reservation_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18495 for statement in [
18496 format!(
18497 "CREATE FUNCTION {schema}.reject_execution_reservation_commit() RETURNS trigger \
18498 LANGUAGE plpgsql AS 'BEGIN RAISE EXCEPTION ''test reservation commit rejection''; \
18499 RETURN NEW; END'"
18500 ),
18501 format!(
18502 "CREATE CONSTRAINT TRIGGER reject_execution_reservation_commit AFTER INSERT ON \
18503 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18504 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_execution_reservation_commit()"
18505 ),
18506 ] {
18507 sqlx::query(sqlx::AssertSqlSafe(statement))
18508 .execute(admin_pool)
18509 .await
18510 .unwrap();
18511 }
18512 }
18513
18514 async fn install_recoverable_commit_rejection(admin_pool: &sqlx::PgPool, schema: &str) {
18515 for statement in [
18516 format!(
18517 "CREATE FUNCTION {schema}.reject_recoverable_commit() RETURNS trigger \
18518 LANGUAGE plpgsql AS 'BEGIN IF NEW.transition_key = ''recoverable'' THEN \
18519 RAISE EXCEPTION ''test recoverable commit rejection''; END IF; \
18520 RETURN NEW; END'"
18521 ),
18522 format!(
18523 "CREATE CONSTRAINT TRIGGER reject_recoverable_commit AFTER INSERT ON \
18524 {schema}.execution_transaction_transition DEFERRABLE INITIALLY DEFERRED \
18525 FOR EACH ROW EXECUTE FUNCTION {schema}.reject_recoverable_commit()"
18526 ),
18527 ] {
18528 sqlx::query(sqlx::AssertSqlSafe(statement))
18529 .execute(admin_pool)
18530 .await
18531 .unwrap();
18532 }
18533 }
18534
18535 async fn drop_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18536 sqlx::query(sqlx::AssertSqlSafe(format!("DROP SCHEMA {schema} CASCADE")))
18537 .execute(admin_pool)
18538 .await
18539 .unwrap();
18540 }
18541
18542 async fn setup_execution_schema(admin_pool: &sqlx::PgPool, schema: &str) {
18543 for statement in [
18544 format!("CREATE SCHEMA {schema}"),
18545 format!(
18546 r#"CREATE TABLE {schema}."chain" (chain_id INTEGER PRIMARY KEY, name TEXT NOT NULL)"#
18547 ),
18548 format!(r#"INSERT INTO {schema}."chain" (chain_id, name) VALUES (42161, 'Arbitrum')"#),
18549 format!(
18550 r#"CREATE TABLE {schema}."execution_transaction" (
18551 id BIGSERIAL PRIMARY KEY,
18552 chain_id INTEGER NOT NULL REFERENCES {schema}."chain"(chain_id) ON DELETE CASCADE,
18553 nonce BIGINT NOT NULL,
18554 transaction_hash TEXT NOT NULL,
18555 purpose TEXT NOT NULL,
18556 status TEXT NOT NULL,
18557 UNIQUE (chain_id, transaction_hash)
18558 )"#
18559 ),
18560 ] {
18561 sqlx::query(sqlx::AssertSqlSafe(statement))
18562 .execute(admin_pool)
18563 .await
18564 .unwrap();
18565 }
18566 }
18567
18568 fn execution_transaction_create_sql() -> &'static str {
18569 const TABLES_SQL: &str = include_str!("../../../../../schema/sql/tables.sql");
18570 const START: &str = "CREATE TABLE IF NOT EXISTS \"execution_transaction\"";
18571 let start = TABLES_SQL
18572 .find(START)
18573 .expect("execution_transaction table is missing from tables.sql");
18574 let statement = &TABLES_SQL[start..];
18575 let end = statement
18576 .find(";\n")
18577 .expect("execution_transaction CREATE TABLE is unterminated")
18578 + 1;
18579 &statement[..end]
18580 }
18581}