Skip to main content

nautilus_blockchain/cache/
database.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{collections::BTreeSet, fmt::Display, pin::Pin, sync::Arc};
17
18use alloy::primitives::{Address, U256};
19use anyhow::Context;
20use futures_util::{Stream, StreamExt};
21use nautilus_model::{
22    defi::{
23        Block, Chain, DexType, Pool, PoolIdentifier, PoolLiquidityUpdate, PoolSwap, SharedChain,
24        SharedDex, Token,
25        data::{
26            DexPoolData, PoolFeeCollect, PoolFeeProtocolCollect, PoolFeeProtocolUpdate, PoolFlash,
27            block::{BLOCK_SCOPED_SNAPSHOT_INDEX, BlockPosition},
28        },
29        pool_analysis::{
30            position::PoolPosition,
31            snapshot::{PoolAnalytics, PoolSnapshot, PoolState},
32        },
33        tick_map::tick::PoolTick,
34        validation::validate_address,
35    },
36    identifiers::InstrumentId,
37};
38use rust_decimal::Decimal;
39use sqlx::{
40    AssertSqlSafe, PgPool, Postgres, Row, Transaction,
41    postgres::{PgAdvisoryLock, PgAdvisoryLockKey, PgConnectOptions},
42};
43
44use crate::{
45    cache::{
46        PoolEventSyncState,
47        consistency::CachedBlocksConsistencyStatus,
48        copy::PostgresCopyHandler,
49        rows::{
50            BlockTimestampRow, ExecutionIntentInsert, ExecutionIntentRow,
51            ExecutionTransactionHashRow, ExecutionTransactionRow, PoolRow, TokenRow,
52            parse_cached_block_timestamp, transform_row_to_dex_pool_data,
53        },
54        types::{U128Pg, U256Pg},
55    },
56    events::initialize::InitializeEvent,
57    execution::{
58        sealing::{
59            PayloadKeySet, PayloadPolicy, authenticate_payload, authenticate_retained_payload,
60            envelope_key_id, payload_context, retained_payload_requires_policy,
61        },
62        transaction::{EXECUTION_SCHEMA_VERSION, TransactionStatus},
63    },
64    rpc::verification::VERIFICATION_SCHEMA_VERSION,
65};
66
67const EXECUTION_PAYLOAD_COMPONENT: &str = "evm_execution_payload";
68const EXECUTION_PAYLOAD_PROTOCOL_VERSION: i16 = 1;
69const EXECUTION_PAYLOAD_BATCH_SIZE: i64 = 100;
70const EXECUTION_PAYLOAD_MAX_SEALS: i64 = 4_294_967_296;
71
72pub(crate) struct ExecutionVerificationBootstrap<'a> {
73    pub chain_id: u32,
74    pub wallet_address: &'a str,
75    pub manifest_version: &'a str,
76    pub manifest_digest: &'a str,
77    pub checkpoint_number: u64,
78    pub checkpoint_hash: &'a str,
79    pub checkpoint_parent_hash: &'a str,
80    pub checkpoint_timestamp: u64,
81    pub checkpoint_base_fee_per_gas: Option<u128>,
82    pub finalized_headers: &'a [ExecutionVerifiedHeader],
83    pub next_canonical_nonce: u64,
84    pub observed_canonical_nonce: u64,
85    pub provider_ids: &'a [String],
86    pub operator_ids: &'a [String],
87    pub failure_domain_ids: &'a [String],
88    pub decisions: &'a [ExecutionVerificationDecision],
89    pub migration: Option<&'a ExecutionVerificationMigration>,
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub(crate) struct ExecutionVerificationMigrationSnapshot {
94    pub intents: Vec<ExecutionIntentRow>,
95    pub hashes: Vec<ExecutionTransactionHashRow>,
96}
97
98pub(crate) struct ExecutionVerificationMigration {
99    pub snapshot: ExecutionVerificationMigrationSnapshot,
100    pub records: Vec<ExecutionVerificationMigrationRecord>,
101}
102
103pub(crate) struct ExecutionVerificationMigrationRecord {
104    pub intent_id: i64,
105    pub nonce: Option<u64>,
106    pub transaction_hash: Option<String>,
107    pub terminal_status: Option<TransactionStatus>,
108    pub block_number: Option<u64>,
109    pub block_hash: Option<String>,
110    pub receipt_success: Option<bool>,
111    pub gas_used: Option<u64>,
112    pub effective_gas_price: Option<String>,
113    pub recover_prepared: bool,
114    pub decisions: Vec<ExecutionVerificationDecision>,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
118pub(crate) struct ExecutionVerifiedHeader {
119    pub number: u64,
120    pub hash: String,
121    pub parent_hash: String,
122    pub timestamp: u64,
123    pub base_fee_per_gas: Option<u128>,
124}
125
126pub(crate) struct ExecutionVerificationPosition {
127    pub next_canonical_nonce: u64,
128    pub revision: u64,
129    pub finalized_tip: ExecutionVerifiedHeader,
130}
131
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub(crate) struct ExecutionVerificationDecision {
134    pub read_class: &'static str,
135    pub height_start: Option<u64>,
136    pub height_end: Option<u64>,
137    pub normalized_value_digest: String,
138}
139
140pub(crate) struct ExecutionNonceAssignment<'a> {
141    pub intent_id: i64,
142    pub chain_id: u32,
143    pub wallet_address: &'a str,
144    pub nonce: u64,
145    pub manifest_version: &'a str,
146    pub manifest_digest: &'a str,
147    pub provider_ids: &'a [String],
148    pub operator_ids: &'a [String],
149    pub failure_domain_ids: &'a [String],
150    pub decisions: &'a [ExecutionVerificationDecision],
151}
152
153pub(crate) struct ExecutionFinalityTransition<'a> {
154    pub intent_id: i64,
155    pub chain_id: u32,
156    pub wallet_address: &'a str,
157    pub nonce: u64,
158    pub transaction_hash: &'a str,
159    pub status: TransactionStatus,
160    pub block_number: u64,
161    pub block_hash: &'a str,
162    pub receipt_success: bool,
163    pub gas_used: u64,
164    pub effective_gas_price: &'a str,
165    pub manifest_version: &'a str,
166    pub manifest_digest: &'a str,
167    pub provider_ids: &'a [String],
168    pub operator_ids: &'a [String],
169    pub failure_domain_ids: &'a [String],
170    pub decisions: &'a [ExecutionVerificationDecision],
171    pub finalized_headers: &'a [ExecutionVerifiedHeader],
172}
173
174pub(crate) struct ExecutionVerificationBatch<'a> {
175    pub intent_id: i64,
176    pub chain_id: u32,
177    pub wallet_address: &'a str,
178    pub nonce: u64,
179    pub decision_class: &'a str,
180    pub manifest_version: &'a str,
181    pub manifest_digest: &'a str,
182    pub provider_ids: &'a [String],
183    pub operator_ids: &'a [String],
184    pub failure_domain_ids: &'a [String],
185    pub decisions: &'a [ExecutionVerificationDecision],
186}
187
188pub(crate) struct ExecutionReplacementScan<'a> {
189    pub intent_id: i64,
190    pub chain_id: u32,
191    pub wallet_address: &'a str,
192    pub nonce: u64,
193    pub finalized_cursor: Option<&'a ExecutionVerifiedHeader>,
194    pub matched_transaction_hash: Option<&'a str>,
195    pub manifest_version: &'a str,
196    pub manifest_digest: &'a str,
197    pub provider_ids: &'a [String],
198    pub operator_ids: &'a [String],
199    pub failure_domain_ids: &'a [String],
200    pub decisions: &'a [ExecutionVerificationDecision],
201}
202const EXECUTION_PAYLOAD_INFRASTRUCTURE_QUERY: &str = "
203    SELECT
204        EXISTS (
205            SELECT 1
206            FROM pg_catalog.pg_trigger AS t
207            JOIN pg_catalog.pg_class AS c ON c.oid = t.tgrelid
208            JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
209            WHERE n.nspname = current_schema()
210              AND c.relname = 'execution_transaction_hash'
211              AND t.tgname = 'execution_transaction_payload_fence'
212              AND NOT t.tgisinternal
213        ),
214        EXISTS (
215            SELECT 1
216            FROM pg_catalog.pg_constraint AS con
217            JOIN pg_catalog.pg_class AS c ON c.oid = con.conrelid
218            JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace
219            WHERE n.nspname = current_schema()
220              AND c.relname = 'execution_transaction_hash'
221              AND con.conname = 'execution_transaction_payload_protected_check'
222              AND con.contype = 'c'
223        )
224";
225
226#[derive(Debug)]
227struct ExecutionPayloadState {
228    deployment_id: String,
229    protocol_version: i16,
230    operation: String,
231    active_key_id: Vec<u8>,
232}
233
234/// Transaction-held lease which prevents payload maintenance from starting during an action.
235pub(crate) struct ExecutionPayloadLease {
236    _transaction: Transaction<'static, Postgres>,
237}
238
239#[derive(Debug, Clone, PartialEq, Eq)]
240pub(crate) struct ExecutionPayloadCheck {
241    pub protected: bool,
242    pub deployment_id: Option<String>,
243    pub plaintext_rows: u64,
244    pub original_rows: u64,
245    pub replacement_rows: u64,
246    pub authenticated_rows: u64,
247    pub key_ids: Vec<String>,
248    pub read_roles: Vec<String>,
249}
250
251/// Database interface for persisting and retrieving blockchain entities and domain objects.
252#[derive(Debug, Clone)]
253pub struct BlockchainCacheDatabase {
254    /// PostgreSQL connection pool used for database operations.
255    pool: PgPool,
256}
257
258#[derive(Debug, Clone, Copy, PartialEq, Eq)]
259enum ExecutionIntentReservationStage {
260    BeforeCommit,
261    Commit,
262}
263
264#[derive(Debug)]
265struct ExecutionIntentReservationError {
266    stage: ExecutionIntentReservationStage,
267    source: anyhow::Error,
268}
269
270impl Display for ExecutionIntentReservationError {
271    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
272        match self.stage {
273            ExecutionIntentReservationStage::BeforeCommit => {
274                f.write_str("Execution intent reservation failed before commit")
275            }
276            ExecutionIntentReservationStage::Commit => f.write_str(
277                "Execution intent reservation commit outcome is unknown; reconciliation is required",
278            ),
279        }
280    }
281}
282
283impl std::error::Error for ExecutionIntentReservationError {
284    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
285        Some(self.source.as_ref())
286    }
287}
288
289pub(crate) fn reservation_failure_proven_not_committed(error: &anyhow::Error) -> bool {
290    error
291        .downcast_ref::<ExecutionIntentReservationError>()
292        .is_some_and(|e| e.stage == ExecutionIntentReservationStage::BeforeCommit)
293}
294
295// Shared SELECT column list for `pool` row queries (load_pools, load_pool).
296const POOL_ROW_COLUMNS: &str = "
297    address,
298    pool_identifier,
299    dex_name,
300    creation_block,
301    COALESCE(
302        (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool.chain_id AND block.number = pool.creation_block),
303        (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool.chain_id AND pool_event_block.number = pool.creation_block)
304    ) as creation_block_timestamp,
305    token0_chain,
306    token0_address,
307    token1_chain,
308    token1_address,
309    fee,
310    tick_spacing,
311    initial_tick,
312    initial_sqrt_price_x96,
313    hook_address
314";
315
316impl BlockchainCacheDatabase {
317    /// Initializes a new database instance by establishing a connection to PostgreSQL.
318    ///
319    /// # Panics
320    ///
321    /// Panics if unable to connect to PostgreSQL with the provided options.
322    pub async fn init(pg_options: PgConnectOptions) -> Self {
323        Self::connect(pg_options)
324            .await
325            .expect("Error connecting to Postgres")
326    }
327
328    /// Establishes a connection to PostgreSQL and returns a new database instance.
329    ///
330    /// # Errors
331    ///
332    /// Returns an error if a connection cannot be established with the provided options.
333    pub async fn connect(pg_options: PgConnectOptions) -> anyhow::Result<Self> {
334        let pool = sqlx::postgres::PgPoolOptions::new()
335            .max_connections(32) // Increased from default 10
336            .min_connections(5) // Keep some connections warm
337            .acquire_timeout(std::time::Duration::from_secs(3))
338            .connect_with(pg_options)
339            .await?;
340        Ok(Self { pool })
341    }
342
343    /// Seeds the database with a blockchain chain record.
344    ///
345    /// # Errors
346    ///
347    /// Returns an error if the database operation fails.
348    pub async fn seed_chain(&self, chain: &Chain) -> anyhow::Result<()> {
349        sqlx::query(
350            "
351            INSERT INTO chain (
352                chain_id, name
353            ) VALUES ($1,$2)
354            ON CONFLICT (chain_id)
355            DO NOTHING
356        ",
357        )
358        .bind(chain.chain_id as i32)
359        .bind(chain.name.to_string())
360        .execute(&self.pool)
361        .await
362        .map(|_| ())
363        .map_err(|e| anyhow::anyhow!("Failed to seed chain table: {e}"))
364    }
365
366    /// Creates a table partition for the block table specific to the given chain
367    /// by calling the existing PostgreSQL function `create_block_partition`.
368    ///
369    /// # Errors
370    ///
371    /// Returns an error if the database operation fails.
372    pub async fn create_block_partition(&self, chain: &Chain) -> anyhow::Result<String> {
373        let result: (String,) = sqlx::query_as("SELECT create_block_partition($1)")
374            .bind(chain.chain_id as i32)
375            .fetch_one(&self.pool)
376            .await
377            .map_err(|e| {
378                anyhow::anyhow!(
379                    "Failed to call create_block_partition for chain {}: {e}",
380                    chain.chain_id
381                )
382            })?;
383
384        Ok(result.0)
385    }
386
387    /// Creates a table partition for the token table specific to the given chain
388    /// by calling the existing PostgreSQL function `create_token_partition`.
389    ///
390    /// # Errors
391    ///
392    /// Returns an error if the database operation fails.
393    pub async fn create_token_partition(&self, chain: &Chain) -> anyhow::Result<String> {
394        let result: (String,) = sqlx::query_as("SELECT create_token_partition($1)")
395            .bind(chain.chain_id as i32)
396            .fetch_one(&self.pool)
397            .await
398            .map_err(|e| {
399                anyhow::anyhow!(
400                    "Failed to call create_token_partition for chain {}: {e}",
401                    chain.chain_id
402                )
403            })?;
404
405        Ok(result.0)
406    }
407
408    /// Returns the highest block number that maintains data continuity in the database.
409    ///
410    /// # Errors
411    ///
412    /// Returns an error if the database query fails.
413    pub async fn get_block_consistency_status(
414        &self,
415        chain: &Chain,
416    ) -> anyhow::Result<CachedBlocksConsistencyStatus> {
417        log::debug!("Fetching block consistency status");
418
419        let result: (i64, i64) = sqlx::query_as(
420            "
421            SELECT
422                COALESCE((SELECT number FROM block WHERE chain_id = $1 ORDER BY number DESC LIMIT 1), 0) as max_block,
423                get_last_continuous_block($1) as last_continuous_block
424            "
425        )
426        .bind(chain.chain_id as i32)
427        .fetch_one(&self.pool)
428        .await
429        .map_err(|e| {
430            anyhow::anyhow!(
431                "Failed to get block info for chain {}: {}",
432                chain.chain_id,
433                e
434            )
435        })?;
436
437        Ok(CachedBlocksConsistencyStatus::new(
438            result.0 as u64,
439            result.1 as u64,
440        ))
441    }
442
443    /// Inserts or updates a block record in the database.
444    ///
445    /// # Errors
446    ///
447    /// Returns an error if the database operation fails.
448    pub async fn add_block(&self, chain_id: u32, block: &Block) -> anyhow::Result<()> {
449        sqlx::query(
450            "
451            INSERT INTO block (
452                chain_id, number, hash, parent_hash, miner, gas_limit, gas_used, timestamp,
453                base_fee_per_gas, blob_gas_used, excess_blob_gas,
454                l1_gas_price, l1_gas_used, l1_fee_scalar
455            ) VALUES (
456                $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
457            )
458            ON CONFLICT (chain_id, number)
459            DO UPDATE
460            SET
461                hash = $3,
462                parent_hash = $4,
463                miner = $5,
464                gas_limit = $6,
465                gas_used = $7,
466                timestamp = $8,
467                base_fee_per_gas = $9,
468                blob_gas_used = $10,
469                excess_blob_gas = $11,
470                l1_gas_price = $12,
471                l1_gas_used = $13,
472                l1_fee_scalar = $14
473        ",
474        )
475        .bind(chain_id as i32)
476        .bind(block.number as i64)
477        .bind(block.hash.as_str())
478        .bind(block.parent_hash.as_str())
479        .bind(block.miner.as_str())
480        .bind(block.gas_limit as i64)
481        .bind(block.gas_used as i64)
482        .bind(block.timestamp.to_string())
483        .bind(block.base_fee_per_gas.as_ref().map(U256::to_string))
484        .bind(block.blob_gas_used.as_ref().map(U256::to_string))
485        .bind(block.excess_blob_gas.as_ref().map(U256::to_string))
486        .bind(block.l1_gas_price.as_ref().map(U256::to_string))
487        .bind(block.l1_gas_used.map(|v| v as i64))
488        .bind(block.l1_fee_scalar.map(|v| v as i64))
489        .execute(&self.pool)
490        .await
491        .map(|_| ())
492        .map_err(|e| anyhow::anyhow!("Failed to insert into block table: {e}"))
493    }
494
495    /// Inserts multiple blocks in a single database operation using UNNEST for optimal performance.
496    ///
497    /// # Errors
498    ///
499    /// Returns an error if the database operation fails.
500    pub async fn add_blocks_batch(&self, chain_id: u32, blocks: &[Block]) -> anyhow::Result<()> {
501        if blocks.is_empty() {
502            return Ok(());
503        }
504
505        // Prepare vectors for each column
506        let mut numbers: Vec<i64> = Vec::with_capacity(blocks.len());
507        let mut hashes: Vec<String> = Vec::with_capacity(blocks.len());
508        let mut parent_hashes: Vec<String> = Vec::with_capacity(blocks.len());
509        let mut miners: Vec<String> = Vec::with_capacity(blocks.len());
510        let mut gas_limits: Vec<i64> = Vec::with_capacity(blocks.len());
511        let mut gas_useds: Vec<i64> = Vec::with_capacity(blocks.len());
512        let mut timestamps: Vec<String> = Vec::with_capacity(blocks.len());
513        let mut base_fee_per_gases: Vec<Option<String>> = Vec::with_capacity(blocks.len());
514        let mut blob_gas_useds: Vec<Option<String>> = Vec::with_capacity(blocks.len());
515        let mut excess_blob_gases: Vec<Option<String>> = Vec::with_capacity(blocks.len());
516        let mut l1_gas_prices: Vec<Option<String>> = Vec::with_capacity(blocks.len());
517        let mut l1_gas_useds: Vec<Option<i64>> = Vec::with_capacity(blocks.len());
518        let mut l1_fee_scalars: Vec<Option<i64>> = Vec::with_capacity(blocks.len());
519
520        // Fill vectors from blocks
521        for block in blocks {
522            numbers.push(block.number as i64);
523            hashes.push(block.hash.clone());
524            parent_hashes.push(block.parent_hash.clone());
525            miners.push(block.miner.to_string());
526            gas_limits.push(block.gas_limit as i64);
527            gas_useds.push(block.gas_used as i64);
528            timestamps.push(block.timestamp.to_string());
529            base_fee_per_gases.push(block.base_fee_per_gas.as_ref().map(U256::to_string));
530            blob_gas_useds.push(block.blob_gas_used.as_ref().map(U256::to_string));
531            excess_blob_gases.push(block.excess_blob_gas.as_ref().map(U256::to_string));
532            l1_gas_prices.push(block.l1_gas_price.as_ref().map(U256::to_string));
533            l1_gas_useds.push(block.l1_gas_used.map(|v| v as i64));
534            l1_fee_scalars.push(block.l1_fee_scalar.map(|v| v as i64));
535        }
536
537        // Execute batch insert with UNNEST
538        sqlx::query(
539            "
540            INSERT INTO block (
541                chain_id, number, hash, parent_hash, miner, gas_limit, gas_used, timestamp,
542                base_fee_per_gas, blob_gas_used, excess_blob_gas,
543                l1_gas_price, l1_gas_used, l1_fee_scalar
544            )
545            SELECT
546                $1, *
547            FROM UNNEST(
548                $2::int8[], $3::text[], $4::text[], $5::text[],
549                $6::int8[], $7::int8[], $8::text[],
550                $9::text[], $10::text[], $11::text[],
551                $12::text[], $13::int8[], $14::int8[]
552            )
553            ON CONFLICT (chain_id, number) DO NOTHING
554           ",
555        )
556        .bind(chain_id as i32)
557        .bind(&numbers[..])
558        .bind(&hashes[..])
559        .bind(&parent_hashes[..])
560        .bind(&miners[..])
561        .bind(&gas_limits[..])
562        .bind(&gas_useds[..])
563        .bind(&timestamps[..])
564        .bind(&base_fee_per_gases as &[Option<String>])
565        .bind(&blob_gas_useds as &[Option<String>])
566        .bind(&excess_blob_gases as &[Option<String>])
567        .bind(&l1_gas_prices as &[Option<String>])
568        .bind(&l1_gas_useds as &[Option<i64>])
569        .bind(&l1_fee_scalars as &[Option<i64>])
570        .execute(&self.pool)
571        .await
572        .map(|_| ())
573        .map_err(|e| anyhow::anyhow!("Failed to batch insert into block table: {e}"))
574    }
575
576    /// Inserts block timestamps observed while streaming pool events.
577    ///
578    /// # Errors
579    ///
580    /// Returns an error if the database operation fails.
581    pub async fn add_pool_event_blocks_batch(
582        &self,
583        chain_id: u32,
584        blocks: &[Block],
585    ) -> anyhow::Result<()> {
586        if blocks.is_empty() {
587            return Ok(());
588        }
589
590        let chain_id_db = i32::try_from(chain_id)
591            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
592        let mut numbers: Vec<i64> = Vec::with_capacity(blocks.len());
593        let mut hashes: Vec<String> = Vec::with_capacity(blocks.len());
594        let mut timestamps: Vec<String> = Vec::with_capacity(blocks.len());
595
596        for block in blocks {
597            numbers.push(i64::try_from(block.number).with_context(|| {
598                format!(
599                    "Pool event block {} exceeds PostgreSQL BIGINT",
600                    block.number
601                )
602            })?);
603            hashes.push(block.hash.clone());
604            timestamps.push(block.timestamp.to_string());
605        }
606
607        sqlx::query(
608            "
609            INSERT INTO pool_event_block (
610                chain_id, number, hash, timestamp
611            )
612            SELECT
613                $1, *
614            FROM UNNEST(
615                $2::int8[], $3::text[], $4::text[]
616            )
617            ON CONFLICT (chain_id, number)
618            DO UPDATE SET hash = EXCLUDED.hash, timestamp = EXCLUDED.timestamp
619           ",
620        )
621        .bind(chain_id_db)
622        .bind(&numbers[..])
623        .bind(&hashes[..])
624        .bind(&timestamps[..])
625        .execute(&self.pool)
626        .await
627        .map(|_| ())
628        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_event_block table: {e}"))
629    }
630
631    /// Adds block-hash storage to databases created before hash-bound profiler checkpoints.
632    ///
633    /// # Errors
634    ///
635    /// Returns an error if the schema update fails.
636    pub async fn ensure_pool_event_block_hash_schema(&self) -> anyhow::Result<()> {
637        sqlx::query("ALTER TABLE pool_event_block ADD COLUMN IF NOT EXISTS hash TEXT")
638            .execute(&self.pool)
639            .await
640            .map(|_| ())
641            .map_err(|e| anyhow::anyhow!("Failed to add pool event block hash storage: {e}"))
642    }
643
644    /// Inserts blocks using PostgreSQL COPY BINARY for maximum performance.
645    ///
646    /// This method is significantly faster than INSERT for bulk operations as it bypasses
647    /// SQL parsing and uses PostgreSQL's native binary protocol.
648    ///
649    /// # Errors
650    ///
651    /// Returns an error if the COPY operation fails.
652    pub async fn add_blocks_copy(&self, chain_id: u32, blocks: &[Block]) -> anyhow::Result<()> {
653        let copy_handler = PostgresCopyHandler::new(&self.pool);
654        copy_handler.copy_blocks(chain_id, blocks).await
655    }
656
657    /// Inserts tokens using PostgreSQL COPY BINARY for maximum performance.
658    ///
659    /// # Errors
660    ///
661    /// Returns an error if the COPY operation fails.
662    pub async fn add_tokens_copy(&self, chain_id: u32, tokens: &[Token]) -> anyhow::Result<()> {
663        let copy_handler = PostgresCopyHandler::new(&self.pool);
664        copy_handler.copy_tokens(chain_id, tokens).await
665    }
666
667    /// Inserts pools using PostgreSQL COPY BINARY for maximum performance.
668    ///
669    /// # Errors
670    ///
671    /// Returns an error if the COPY operation fails.
672    pub async fn add_pools_copy(&self, chain_id: u32, pools: &[Pool]) -> anyhow::Result<()> {
673        let copy_handler = PostgresCopyHandler::new(&self.pool);
674        copy_handler.copy_pools(chain_id, pools).await
675    }
676
677    /// Inserts pool swaps using PostgreSQL COPY BINARY for maximum performance.
678    ///
679    /// This method is significantly faster than INSERT for bulk operations as it bypasses
680    /// SQL parsing and uses PostgreSQL's native binary protocol.
681    ///
682    /// # Errors
683    ///
684    /// Returns an error if the COPY operation fails.
685    pub async fn add_pool_swaps_copy(
686        &self,
687        chain_id: u32,
688        swaps: &[PoolSwap],
689    ) -> anyhow::Result<()> {
690        let copy_handler = PostgresCopyHandler::new(&self.pool);
691        copy_handler.copy_pool_swaps(chain_id, swaps).await
692    }
693
694    /// Inserts pool liquidity updates using PostgreSQL COPY BINARY for maximum performance.
695    ///
696    /// This method is significantly faster than INSERT for bulk operations as it bypasses
697    /// SQL parsing and uses PostgreSQL's native binary protocol.
698    ///
699    /// # Errors
700    ///
701    /// Returns an error if the COPY operation fails.
702    pub async fn add_pool_liquidity_updates_copy(
703        &self,
704        chain_id: u32,
705        updates: &[PoolLiquidityUpdate],
706    ) -> anyhow::Result<()> {
707        let copy_handler = PostgresCopyHandler::new(&self.pool);
708        copy_handler
709            .copy_pool_liquidity_updates(chain_id, updates)
710            .await
711    }
712
713    /// Inserts pool fee collect events using PostgreSQL COPY BINARY for maximum performance.
714    ///
715    /// This method is significantly faster than INSERT for bulk operations as it bypasses
716    /// SQL parsing and most database validation checks.
717    ///
718    /// # Errors
719    ///
720    /// Returns an error if the COPY operation fails.
721    pub async fn copy_pool_fee_collects_batch(
722        &self,
723        chain_id: u32,
724        collects: &[PoolFeeCollect],
725    ) -> anyhow::Result<()> {
726        let copy_handler = PostgresCopyHandler::new(&self.pool);
727        copy_handler.copy_pool_collects(chain_id, collects).await
728    }
729
730    /// Retrieves block timestamps for a given chain starting from a specific block number.
731    ///
732    /// # Errors
733    ///
734    /// Returns an error if the database query fails.
735    pub async fn load_block_timestamps(
736        &self,
737        chain: SharedChain,
738        from_block: u64,
739    ) -> anyhow::Result<Vec<BlockTimestampRow>> {
740        sqlx::query_as::<_, BlockTimestampRow>(
741            "
742            SELECT DISTINCT ON (number)
743                number,
744                timestamp
745            FROM (
746                SELECT number, timestamp, 0 AS source_order
747                FROM block
748                WHERE chain_id = $1 AND number >= $2 AND timestamp IS NOT NULL
749                UNION ALL
750                SELECT number, timestamp, 1 AS source_order
751                FROM pool_event_block
752                WHERE chain_id = $1 AND number >= $2 AND timestamp IS NOT NULL
753            ) AS block_timestamps
754            ORDER BY number ASC, source_order ASC
755            ",
756        )
757        .bind(chain.chain_id as i32)
758        .bind(from_block as i64)
759        .fetch_all(&self.pool)
760        .await
761        .map_err(|e| anyhow::anyhow!("Failed to load block timestamps: {e}"))
762    }
763
764    /// Adds or updates a DEX (Decentralized Exchange) record in the database.
765    ///
766    /// # Errors
767    ///
768    /// Returns an error if the database operation fails.
769    pub async fn add_dex(&self, dex: SharedDex) -> anyhow::Result<()> {
770        sqlx::query(
771            "
772            INSERT INTO dex (
773                chain_id, name, factory_address, creation_block
774            ) VALUES ($1, $2, $3, $4)
775            ON CONFLICT (chain_id, name)
776            DO UPDATE
777            SET
778                factory_address = $3,
779                creation_block = $4
780        ",
781        )
782        .bind(dex.chain.chain_id as i32)
783        .bind(dex.name.to_string())
784        .bind(dex.factory.to_string())
785        .bind(dex.factory_creation_block as i64)
786        .execute(&self.pool)
787        .await
788        .map(|_| ())
789        .map_err(|e| anyhow::anyhow!("Failed to insert into dex table: {e}"))
790    }
791
792    /// Adds or updates a liquidity pool/pair record in the database.
793    ///
794    /// # Errors
795    ///
796    /// Returns an error if the database operation fails.
797    pub async fn add_pool(&self, pool: &Pool) -> anyhow::Result<()> {
798        sqlx::query(
799            "
800            INSERT INTO pool (
801                chain_id, address, pool_identifier, dex_name, creation_block,
802                token0_chain, token0_address,
803                token1_chain, token1_address,
804                fee, tick_spacing, initial_tick, initial_sqrt_price_x96, hook_address
805            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
806            ON CONFLICT (chain_id, dex_name, pool_identifier)
807            DO UPDATE
808            SET
809                address = $2,
810                creation_block = $5,
811                token0_chain = $6,
812                token0_address = $7,
813                token1_chain = $8,
814                token1_address = $9,
815                fee = $10,
816                tick_spacing = $11,
817                initial_tick = $12,
818                initial_sqrt_price_x96 = $13,
819                hook_address = $14
820        ",
821        )
822        .bind(pool.chain.chain_id as i32)
823        .bind(pool.address.to_string())
824        .bind(pool.pool_identifier.as_ref())
825        .bind(pool.dex.name.to_string())
826        .bind(pool.creation_block as i64)
827        .bind(pool.token0.chain.chain_id as i32)
828        .bind(pool.token0.address.to_string())
829        .bind(pool.token1.chain.chain_id as i32)
830        .bind(pool.token1.address.to_string())
831        .bind(pool.fee.map(|fee| fee as i32))
832        .bind(pool.tick_spacing.map(|tick_spacing| tick_spacing as i32))
833        .bind(pool.initial_tick)
834        .bind(pool.initial_sqrt_price_x96.as_ref().map(|p| p.to_string()))
835        .bind(pool.hooks.as_ref().map(|h| h.to_string()))
836        .execute(&self.pool)
837        .await
838        .map(|_| ())
839        .map_err(|e| anyhow::anyhow!("Failed to insert into pool table: {e}"))
840    }
841
842    /// Inserts multiple pools in a single database operation using UNNEST for optimal performance.
843    ///
844    /// # Errors
845    ///
846    /// Returns an error if the database operation fails.
847    pub async fn add_pools_batch(&self, pools: &[Pool]) -> anyhow::Result<()> {
848        if pools.is_empty() {
849            return Ok(());
850        }
851
852        // Prepare vectors for each column
853        let len = pools.len();
854        let mut addresses: Vec<String> = Vec::with_capacity(len);
855        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
856        let mut dex_names: Vec<String> = Vec::with_capacity(len);
857        let mut creation_blocks: Vec<i64> = Vec::with_capacity(len);
858        let mut token0_chains: Vec<i32> = Vec::with_capacity(len);
859        let mut token0_addresses: Vec<String> = Vec::with_capacity(len);
860        let mut token1_chains: Vec<i32> = Vec::with_capacity(len);
861        let mut token1_addresses: Vec<String> = Vec::with_capacity(len);
862        let mut fees: Vec<Option<i32>> = Vec::with_capacity(len);
863        let mut tick_spacings: Vec<Option<i32>> = Vec::with_capacity(len);
864        let mut initial_ticks: Vec<Option<i32>> = Vec::with_capacity(len);
865        let mut initial_sqrt_price_x96s: Vec<Option<String>> = Vec::with_capacity(len);
866        let mut hook_addresses: Vec<Option<String>> = Vec::with_capacity(len);
867        let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
868
869        // Fill vectors from pools
870        for pool in pools {
871            chain_ids.push(pool.chain.chain_id as i32);
872            addresses.push(pool.address.to_string());
873            pool_identifiers.push(pool.pool_identifier.to_string());
874            dex_names.push(pool.dex.name.to_string());
875            creation_blocks.push(pool.creation_block as i64);
876            token0_chains.push(pool.token0.chain.chain_id as i32);
877            token0_addresses.push(pool.token0.address.to_string());
878            token1_chains.push(pool.token1.chain.chain_id as i32);
879            token1_addresses.push(pool.token1.address.to_string());
880            fees.push(pool.fee.map(|fee| fee as i32));
881            tick_spacings.push(pool.tick_spacing.map(|tick_spacing| tick_spacing as i32));
882            initial_ticks.push(pool.initial_tick);
883            initial_sqrt_price_x96s
884                .push(pool.initial_sqrt_price_x96.as_ref().map(|p| p.to_string()));
885            hook_addresses.push(pool.hooks.as_ref().map(|h| h.to_string()));
886        }
887
888        // Execute batch insert with UNNEST
889        sqlx::query(
890            "
891            INSERT INTO pool (
892                chain_id, address, pool_identifier, dex_name, creation_block,
893                token0_chain, token0_address,
894                token1_chain, token1_address,
895                fee, tick_spacing, initial_tick, initial_sqrt_price_x96, hook_address
896            )
897            SELECT *
898            FROM UNNEST(
899                $1::int4[], $2::text[], $3::text[], $4::text[], $5::int8[],
900                $6::int4[], $7::text[], $8::int4[], $9::text[],
901                $10::int4[], $11::int4[], $12::int4[], $13::text[], $14::text[]
902            )
903            ON CONFLICT (chain_id, dex_name, pool_identifier) DO NOTHING
904           ",
905        )
906        .bind(&chain_ids[..])
907        .bind(&addresses[..])
908        .bind(&pool_identifiers[..])
909        .bind(&dex_names[..])
910        .bind(&creation_blocks[..])
911        .bind(&token0_chains[..])
912        .bind(&token0_addresses[..])
913        .bind(&token1_chains[..])
914        .bind(&token1_addresses[..])
915        .bind(&fees[..])
916        .bind(&tick_spacings[..])
917        .bind(&initial_ticks[..])
918        .bind(&initial_sqrt_price_x96s[..])
919        .bind(&hook_addresses as &[Option<String>])
920        .execute(&self.pool)
921        .await
922        .map(|_| ())
923        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool table: {e}"))
924    }
925
926    /// Inserts multiple pool swaps in a single database operation using UNNEST for optimal performance.
927    ///
928    /// # Errors
929    ///
930    /// Returns an error if the database operation fails.
931    pub async fn add_pool_swaps_batch(
932        &self,
933        chain_id: u32,
934        swaps: &[PoolSwap],
935    ) -> anyhow::Result<()> {
936        if swaps.is_empty() {
937            return Ok(());
938        }
939
940        // Prepare vectors for each column
941        let len = swaps.len();
942        let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
943        let mut dex_names: Vec<String> = Vec::with_capacity(len);
944        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
945        let mut blocks: Vec<i64> = Vec::with_capacity(len);
946        let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
947        let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
948        let mut log_indices: Vec<i32> = Vec::with_capacity(len);
949        let mut senders: Vec<String> = Vec::with_capacity(len);
950        let mut recipients: Vec<String> = Vec::with_capacity(len);
951        let mut sqrt_price_x96s: Vec<String> = Vec::with_capacity(len);
952        let mut liquidities: Vec<String> = Vec::with_capacity(len);
953        let mut ticks: Vec<i32> = Vec::with_capacity(len);
954        let mut amount0s: Vec<String> = Vec::with_capacity(len);
955        let mut amount1s: Vec<String> = Vec::with_capacity(len);
956        let mut order_sides: Vec<Option<String>> = Vec::with_capacity(len);
957        let mut base_quantities: Vec<Option<Decimal>> = Vec::with_capacity(len);
958        let mut quote_quantities: Vec<Option<Decimal>> = Vec::with_capacity(len);
959        let mut spot_prices: Vec<Option<Decimal>> = Vec::with_capacity(len);
960        let mut execution_prices: Vec<Option<Decimal>> = Vec::with_capacity(len);
961
962        // Fill vectors from swaps
963        for swap in swaps {
964            chain_ids.push(chain_id as i32);
965            dex_names.push(swap.dex.name.to_string());
966            pool_identifiers.push(swap.pool_identifier.to_string());
967            blocks.push(swap.block as i64);
968            transaction_hashes.push(swap.transaction_hash.clone());
969            transaction_indices.push(swap.transaction_index as i32);
970            log_indices.push(swap.log_index as i32);
971            senders.push(swap.sender.to_string());
972            recipients.push(swap.recipient.to_string());
973            sqrt_price_x96s.push(swap.sqrt_price_x96.to_string());
974            liquidities.push(swap.liquidity.to_string());
975            ticks.push(swap.tick);
976            amount0s.push(swap.amount0.to_string());
977            amount1s.push(swap.amount1.to_string());
978
979            // Extract trade_info fields if available
980            if let Some(ref trade_info) = swap.trade_info {
981                order_sides.push(Some(trade_info.order_side.to_string()));
982                base_quantities.push(Some(trade_info.quantity_base.as_decimal()));
983                quote_quantities.push(Some(trade_info.quantity_quote.as_decimal()));
984                spot_prices.push(Some(trade_info.spot_price.as_decimal()));
985                execution_prices.push(Some(trade_info.execution_price.as_decimal()));
986            } else {
987                order_sides.push(None);
988                base_quantities.push(None);
989                quote_quantities.push(None);
990                spot_prices.push(None);
991                execution_prices.push(None);
992            }
993        }
994
995        // Execute batch insert with UNNEST
996        sqlx::query(
997            "
998            INSERT INTO pool_swap_event (
999                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1000                log_index, sender, recipient, sqrt_price_x96, liquidity, tick, amount0, amount1,
1001                order_side, base_quantity, quote_quantity, spot_price, execution_price
1002            )
1003            SELECT
1004                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index, log_index, sender, recipient,
1005                sqrt_price_x96::U160, liquidity::U128, tick, amount0::I256, amount1::I256,
1006                order_side, base_quantity, quote_quantity, spot_price, execution_price
1007            FROM UNNEST(
1008                $1::INT[], $2::TEXT[], $3::TEXT[], $4::BIGINT[], $5::TEXT[], $6::INT[], $7::INT[],
1009                $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::TEXT[], $12::INT[], $13::TEXT[], $14::TEXT[],
1010                $15::TEXT[], $16, $17, $18, $19
1011            ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1012                   log_index, sender, recipient, sqrt_price_x96, liquidity, tick, amount0, amount1,
1013                   order_side, base_quantity, quote_quantity, spot_price, execution_price)
1014            ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1015           ",
1016        )
1017        .bind(&chain_ids[..])
1018        .bind(&dex_names[..])
1019        .bind(&pool_identifiers[..])
1020        .bind(&blocks[..])
1021        .bind(&transaction_hashes[..])
1022        .bind(&transaction_indices[..])
1023        .bind(&log_indices[..])
1024        .bind(&senders[..])
1025        .bind(&recipients[..])
1026        .bind(&sqrt_price_x96s[..])
1027        .bind(&liquidities[..])
1028        .bind(&ticks[..])
1029        .bind(&amount0s[..])
1030        .bind(&amount1s[..])
1031        .bind(&order_sides[..])
1032        .bind(&base_quantities[..])
1033        .bind(&quote_quantities[..])
1034        .bind(&spot_prices[..])
1035        .bind(&execution_prices[..])
1036        .execute(&self.pool)
1037        .await
1038        .map(|_| ())
1039        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_swap_event table: {e}"))
1040    }
1041
1042    /// Inserts multiple pool liquidity updates in a single database operation using UNNEST for optimal performance.
1043    ///
1044    /// # Errors
1045    ///
1046    /// Returns an error if the database operation fails.
1047    pub async fn add_pool_liquidity_updates_batch(
1048        &self,
1049        chain_id: u32,
1050        updates: &[PoolLiquidityUpdate],
1051    ) -> anyhow::Result<()> {
1052        if updates.is_empty() {
1053            return Ok(());
1054        }
1055
1056        // Prepare vectors for each column
1057        let len = updates.len();
1058        let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1059        let mut dex_names: Vec<String> = Vec::with_capacity(len);
1060        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1061        let mut blocks: Vec<i64> = Vec::with_capacity(len);
1062        let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1063        let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1064        let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1065        let mut event_types: Vec<String> = Vec::with_capacity(len);
1066        let mut senders: Vec<Option<String>> = Vec::with_capacity(len);
1067        let mut owners: Vec<String> = Vec::with_capacity(len);
1068        let mut position_liquidities: Vec<String> = Vec::with_capacity(len);
1069        let mut amount0s: Vec<String> = Vec::with_capacity(len);
1070        let mut amount1s: Vec<String> = Vec::with_capacity(len);
1071        let mut tick_lowers: Vec<i32> = Vec::with_capacity(len);
1072        let mut tick_uppers: Vec<i32> = Vec::with_capacity(len);
1073
1074        // Fill vectors from updates
1075        for update in updates {
1076            chain_ids.push(chain_id as i32);
1077            dex_names.push(update.dex.name.to_string());
1078            pool_identifiers.push(update.pool_identifier.to_string());
1079            blocks.push(update.block as i64);
1080            transaction_hashes.push(update.transaction_hash.clone());
1081            transaction_indices.push(update.transaction_index as i32);
1082            log_indices.push(update.log_index as i32);
1083            event_types.push(update.kind.to_string());
1084            senders.push(update.sender.map(|s| s.to_string()));
1085            owners.push(update.owner.to_string());
1086            position_liquidities.push(update.position_liquidity.to_string());
1087            amount0s.push(update.amount0.to_string());
1088            amount1s.push(update.amount1.to_string());
1089            tick_lowers.push(update.tick_lower);
1090            tick_uppers.push(update.tick_upper);
1091        }
1092
1093        // Execute batch insert with UNNEST
1094        sqlx::query(
1095            "
1096            INSERT INTO pool_liquidity_event (
1097                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1098                log_index, event_type, sender, owner, position_liquidity,
1099                amount0, amount1, tick_lower, tick_upper
1100            )
1101            SELECT
1102                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1103                log_index, event_type, sender, owner, position_liquidity::u128,
1104                amount0::U256, amount1::U256, tick_lower, tick_upper
1105            FROM UNNEST(
1106                $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
1107                $7::INT[], $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::TEXT[],
1108                $12::TEXT[], $13::TEXT[], $14::INT[], $15::INT[]
1109            ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1110                   log_index, event_type, sender, owner, position_liquidity,
1111                   amount0, amount1, tick_lower, tick_upper)
1112            ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1113           ",
1114        )
1115        .bind(&chain_ids[..])
1116        .bind(&dex_names[..])
1117        .bind(&pool_identifiers[..])
1118        .bind(&blocks[..])
1119        .bind(&transaction_hashes[..])
1120        .bind(&transaction_indices[..])
1121        .bind(&log_indices[..])
1122        .bind(&event_types[..])
1123        .bind(&senders[..])
1124        .bind(&owners[..])
1125        .bind(&position_liquidities[..])
1126        .bind(&amount0s[..])
1127        .bind(&amount1s[..])
1128        .bind(&tick_lowers[..])
1129        .bind(&tick_uppers[..])
1130        .execute(&self.pool)
1131        .await
1132        .map(|_| ())
1133        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_liquidity_event table: {e}"))
1134    }
1135
1136    /// Adds or updates a token record in the database.
1137    ///
1138    /// # Errors
1139    ///
1140    /// Returns an error if the database operation fails.
1141    pub async fn add_token(&self, token: &Token) -> anyhow::Result<()> {
1142        sqlx::query(
1143            "
1144            INSERT INTO token (
1145                chain_id, address, name, symbol, decimals
1146            ) VALUES ($1, $2, $3, $4, $5)
1147            ON CONFLICT (chain_id, address)
1148            DO UPDATE
1149            SET
1150                name = $3,
1151                symbol = $4,
1152                decimals = $5
1153        ",
1154        )
1155        .bind(token.chain.chain_id as i32)
1156        .bind(token.address.to_string())
1157        .bind(token.name.as_str())
1158        .bind(token.symbol.as_str())
1159        .bind(i32::from(token.decimals))
1160        .execute(&self.pool)
1161        .await
1162        .map(|_| ())
1163        .map_err(|e| anyhow::anyhow!("Failed to insert into token table: {e}"))
1164    }
1165
1166    /// Records an invalid token address with associated error information.
1167    ///
1168    /// # Errors
1169    ///
1170    /// Returns an error if the database insertion fails.
1171    pub async fn add_invalid_token(
1172        &self,
1173        chain_id: u32,
1174        address: &Address,
1175        error_string: &str,
1176    ) -> anyhow::Result<()> {
1177        sqlx::query(
1178            "
1179            INSERT INTO token (
1180                chain_id, address, error
1181            ) VALUES ($1, $2, $3)
1182            ON CONFLICT (chain_id, address)
1183            DO NOTHING;
1184        ",
1185        )
1186        .bind(chain_id as i32)
1187        .bind(address.to_string())
1188        .bind(error_string)
1189        .execute(&self.pool)
1190        .await
1191        .map(|_| ())
1192        .map_err(|e| anyhow::anyhow!("Failed to insert into token table: {e}"))
1193    }
1194
1195    /// Persists a token swap transaction event to the `pool_swap` table.
1196    ///
1197    /// # Errors
1198    ///
1199    /// Returns an error if the database operation fails.
1200    pub async fn add_swap(&self, chain_id: u32, swap: &PoolSwap) -> anyhow::Result<()> {
1201        // Extract trade_info fields if available
1202        let (order_side, base_quantity, quote_quantity, spot_price, execution_price) =
1203            if let Some(ref trade_info) = swap.trade_info {
1204                (
1205                    Some(trade_info.order_side.to_string()),
1206                    Some(trade_info.quantity_base.as_decimal()),
1207                    Some(trade_info.quantity_quote.as_decimal()),
1208                    Some(trade_info.spot_price.as_decimal()),
1209                    Some(trade_info.execution_price.as_decimal()),
1210                )
1211            } else {
1212                (None, None, None, None, None)
1213            };
1214
1215        sqlx::query(
1216            "
1217            INSERT INTO pool_swap_event (
1218                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1219                log_index, sender, recipient, sqrt_price_x96, liquidity, tick, amount0, amount1,
1220                order_side, base_quantity, quote_quantity, spot_price, execution_price
1221            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::U160, $11::U128, $12, $13::I256, $14::I256, $15, $16, $17, $18, $19)
1222            ON CONFLICT (chain_id, transaction_hash, log_index)
1223            DO NOTHING
1224        ",
1225        )
1226        .bind(chain_id as i32)
1227        .bind(swap.dex.name.to_string())
1228        .bind(swap.pool_identifier.as_str())
1229        .bind(swap.block as i64)
1230        .bind(swap.transaction_hash.as_str())
1231        .bind(swap.transaction_index as i32)
1232        .bind(swap.log_index as i32)
1233        .bind(swap.sender.to_string())
1234        .bind(swap.recipient.to_string())
1235        .bind(swap.sqrt_price_x96.to_string())
1236        .bind(swap.liquidity.to_string())
1237        .bind(swap.tick)
1238        .bind(swap.amount0.to_string())
1239        .bind(swap.amount1.to_string())
1240        .bind(order_side)
1241        .bind(base_quantity)
1242        .bind(quote_quantity)
1243        .bind(spot_price)
1244        .bind(execution_price)
1245        .execute(&self.pool)
1246        .await
1247        .map(|_| ())
1248        .map_err(|e| anyhow::anyhow!("Failed to insert into pool_swap table: {e}"))
1249    }
1250
1251    /// Persists a liquidity position change (mint/burn) event to the `pool_liquidity` table.
1252    ///
1253    /// # Errors
1254    ///
1255    /// Returns an error if the database operation fails.
1256    pub async fn add_pool_liquidity_update(
1257        &self,
1258        chain_id: u32,
1259        liquidity_update: &PoolLiquidityUpdate,
1260    ) -> anyhow::Result<()> {
1261        sqlx::query(
1262            "
1263            INSERT INTO pool_liquidity_event (
1264                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index, log_index,
1265                event_type, sender, owner, position_liquidity, amount0, amount1, tick_lower, tick_upper
1266            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
1267            ON CONFLICT (chain_id, transaction_hash, log_index)
1268            DO NOTHING
1269        ",
1270        )
1271        .bind(chain_id as i32)
1272        .bind(liquidity_update.dex.name.to_string())
1273        .bind(liquidity_update.pool_identifier.as_str())
1274        .bind(liquidity_update.block as i64)
1275        .bind(liquidity_update.transaction_hash.as_str())
1276        .bind(liquidity_update.transaction_index as i32)
1277        .bind(liquidity_update.log_index as i32)
1278        .bind(liquidity_update.kind.to_string())
1279        .bind(liquidity_update.sender.map(|sender| sender.to_string()))
1280        .bind(liquidity_update.owner.to_string())
1281        .bind(U128Pg(liquidity_update.position_liquidity))
1282        .bind(U256Pg(liquidity_update.amount0))
1283        .bind(U256Pg(liquidity_update.amount1))
1284        .bind(liquidity_update.tick_lower)
1285        .bind(liquidity_update.tick_upper)
1286        .execute(&self.pool)
1287        .await
1288        .map(|_| ())
1289        .map_err(|e| anyhow::anyhow!("Failed to insert into pool_liquidity table: {e}"))
1290    }
1291
1292    /// Retrieves all valid token records for the given chain and converts them into `Token` domain objects.
1293    ///
1294    /// Only returns tokens that do not contain error information, filtering out invalid tokens
1295    /// that were previously recorded with error details.
1296    ///
1297    /// # Errors
1298    ///
1299    /// Returns an error if the database query fails.
1300    pub async fn load_tokens(&self, chain: SharedChain) -> anyhow::Result<Vec<Token>> {
1301        sqlx::query_as::<_, TokenRow>("SELECT * FROM token WHERE chain_id = $1 AND error IS NULL")
1302            .bind(chain.chain_id as i32)
1303            .fetch_all(&self.pool)
1304            .await
1305            .map(|rows| {
1306                rows.into_iter()
1307                    .map(|token_row| {
1308                        Token::new(
1309                            Arc::clone(&chain),
1310                            token_row.address,
1311                            token_row.name,
1312                            token_row.symbol,
1313                            token_row.decimals,
1314                        )
1315                    })
1316                    .collect::<Vec<_>>()
1317            })
1318            .map_err(|e| anyhow::anyhow!("Failed to load tokens: {e}"))
1319    }
1320
1321    /// Retrieves all invalid token addresses for a given chain.
1322    ///
1323    /// # Errors
1324    ///
1325    /// Returns an error if the database query fails or address validation fails.
1326    pub async fn load_invalid_token_addresses(
1327        &self,
1328        chain_id: u32,
1329    ) -> anyhow::Result<Vec<Address>> {
1330        sqlx::query_as::<_, (String,)>(
1331            "SELECT address FROM token WHERE chain_id = $1 AND error IS NOT NULL",
1332        )
1333        .bind(chain_id as i32)
1334        .fetch_all(&self.pool)
1335        .await?
1336        .into_iter()
1337        .map(|(address,)| validate_address(&address))
1338        .collect::<Result<Vec<_>, _>>()
1339        .map_err(|e| anyhow::anyhow!("Failed to load invalid token addresses: {e}"))
1340    }
1341
1342    /// Loads pool data from the database for the specified chain and DEX.
1343    ///
1344    /// # Errors
1345    ///
1346    /// Returns an error if the database query fails, the connection to the database is lost, or the query parameters are invalid.
1347    pub async fn load_pools(
1348        &self,
1349        chain: SharedChain,
1350        dex_id: &str,
1351    ) -> anyhow::Result<Vec<PoolRow>> {
1352        sqlx::query_as::<_, PoolRow>(AssertSqlSafe(format!(
1353            "SELECT {POOL_ROW_COLUMNS} FROM pool WHERE chain_id = $1 AND dex_name = $2 ORDER BY creation_block ASC"
1354        )))
1355        .bind(chain.chain_id as i32)
1356        .bind(dex_id)
1357        .fetch_all(&self.pool)
1358        .await
1359        .map_err(|e| anyhow::anyhow!("Failed to load pools: {e}"))
1360    }
1361
1362    /// Loads a single pool row by its identifier.
1363    ///
1364    /// Returns `None` when the pool is not present in the database. Lets per-pool tools load only
1365    /// the pool they analyze instead of the whole DEX pool set (see [`load_pools`]).
1366    ///
1367    /// [`load_pools`]: Self::load_pools
1368    ///
1369    /// # Errors
1370    ///
1371    /// Returns an error if the database query fails.
1372    pub async fn load_pool(
1373        &self,
1374        chain: SharedChain,
1375        dex_id: &str,
1376        pool_identifier: &PoolIdentifier,
1377    ) -> anyhow::Result<Option<PoolRow>> {
1378        sqlx::query_as::<_, PoolRow>(AssertSqlSafe(format!(
1379            "SELECT {POOL_ROW_COLUMNS} FROM pool WHERE chain_id = $1 AND dex_name = $2 AND pool_identifier = $3"
1380        )))
1381        .bind(chain.chain_id as i32)
1382        .bind(dex_id)
1383        .bind(pool_identifier.as_ref())
1384        .fetch_optional(&self.pool)
1385        .await
1386        .map_err(|e| anyhow::anyhow!("Failed to load pool {pool_identifier}: {e}"))
1387    }
1388
1389    /// Toggles performance optimization settings for sync operations.
1390    ///
1391    /// When enabled (true), applies settings for maximum write performance:
1392    /// - `synchronous_commit` = OFF
1393    /// - `work_mem` increased for bulk operations
1394    ///
1395    /// When disabled (false), restores default safe settings:
1396    /// - `synchronous_commit` = ON (data safety)
1397    /// - `work_mem` back to default
1398    ///
1399    /// # Errors
1400    ///
1401    /// Returns an error if the database operations fail.
1402    pub async fn toggle_perf_sync_settings(&self, enable: bool) -> anyhow::Result<()> {
1403        if enable {
1404            log::debug!("Enabling performance sync settings for bulk operations");
1405
1406            // Set synchronous_commit to OFF for maximum write performance
1407            sqlx::query("SET synchronous_commit = OFF")
1408                .execute(&self.pool)
1409                .await
1410                .map_err(|e| anyhow::anyhow!("Failed to set synchronous_commit OFF: {e}"))?;
1411
1412            // Increase work_mem for bulk operations
1413            sqlx::query("SET work_mem = '256MB'")
1414                .execute(&self.pool)
1415                .await
1416                .map_err(|e| anyhow::anyhow!("Failed to set work_mem: {e}"))?;
1417
1418            log::debug!("Performance settings enabled: synchronous_commit=OFF, work_mem=256MB");
1419        } else {
1420            log::debug!("Restoring default safe database performance settings");
1421
1422            // Restore synchronous_commit to ON for data safety
1423            sqlx::query("SET synchronous_commit = ON")
1424                .execute(&self.pool)
1425                .await
1426                .map_err(|e| anyhow::anyhow!("Failed to set synchronous_commit ON: {e}"))?;
1427
1428            // Reset work_mem to default
1429            sqlx::query("RESET work_mem")
1430                .execute(&self.pool)
1431                .await
1432                .map_err(|e| anyhow::anyhow!("Failed to reset work_mem: {e}"))?;
1433        }
1434
1435        Ok(())
1436    }
1437
1438    /// Saves the checkpoint block number indicating the last completed pool synchronization for a specific DEX.
1439    ///
1440    /// # Errors
1441    ///
1442    /// Returns an error if the database operation fails.
1443    pub async fn update_dex_last_synced_block(
1444        &self,
1445        chain_id: u32,
1446        dex: &DexType,
1447        block_number: u64,
1448    ) -> anyhow::Result<()> {
1449        sqlx::query(
1450            "
1451            UPDATE dex
1452            SET last_full_sync_pools_block_number = $3
1453            WHERE chain_id = $1 AND name = $2
1454            ",
1455        )
1456        .bind(chain_id as i32)
1457        .bind(dex.to_string())
1458        .bind(block_number as i64)
1459        .execute(&self.pool)
1460        .await
1461        .map(|_| ())
1462        .map_err(|e| anyhow::anyhow!("Failed to update dex last synced block: {e}"))
1463    }
1464
1465    /// Updates the last synced block number for a pool.
1466    ///
1467    /// # Errors
1468    ///
1469    /// Returns an error if the database update fails.
1470    pub async fn update_pool_last_synced_block(
1471        &self,
1472        chain_id: u32,
1473        dex: &DexType,
1474        pool_identifier: &PoolIdentifier,
1475        block_number: u64,
1476    ) -> anyhow::Result<()> {
1477        sqlx::query(
1478            "
1479            UPDATE pool
1480            SET last_full_sync_block_number = $4
1481            WHERE chain_id = $1
1482            AND dex_name = $2
1483            AND pool_identifier = $3
1484            ",
1485        )
1486        .bind(chain_id as i32)
1487        .bind(dex.to_string())
1488        .bind(pool_identifier.as_ref())
1489        .bind(block_number as i64)
1490        .execute(&self.pool)
1491        .await
1492        .map(|_| ())
1493        .map_err(|e| anyhow::anyhow!("Failed to update pool last synced block: {e}"))
1494    }
1495
1496    /// Retrieves the saved checkpoint block number from the last completed pool synchronization for a specific DEX.
1497    ///
1498    /// # Errors
1499    ///
1500    /// Returns an error if the database query fails.
1501    pub async fn get_dex_last_synced_block(
1502        &self,
1503        chain_id: u32,
1504        dex: &DexType,
1505    ) -> anyhow::Result<Option<u64>> {
1506        let result = sqlx::query_as::<_, (Option<i64>,)>(
1507            "
1508            SELECT
1509                last_full_sync_pools_block_number
1510            FROM dex
1511            WHERE chain_id = $1
1512            AND name = $2
1513            ",
1514        )
1515        .bind(chain_id as i32)
1516        .bind(dex.to_string())
1517        .fetch_optional(&self.pool)
1518        .await
1519        .map_err(|e| anyhow::anyhow!("Failed to get dex last synced block: {e}"))?;
1520
1521        Ok(result.and_then(|(block_number,)| block_number.map(|b| b as u64)))
1522    }
1523
1524    /// Retrieves the last synced block number for a pool.
1525    ///
1526    /// # Errors
1527    ///
1528    /// Returns an error if the database query fails.
1529    pub async fn get_pool_last_synced_block(
1530        &self,
1531        chain_id: u32,
1532        dex: &DexType,
1533        pool_identifier: &PoolIdentifier,
1534    ) -> anyhow::Result<Option<u64>> {
1535        let result = sqlx::query_as::<_, (Option<i64>,)>(
1536            "
1537            SELECT
1538                last_full_sync_block_number
1539            FROM pool
1540            WHERE chain_id = $1
1541            AND dex_name = $2
1542            AND pool_identifier = $3
1543            ",
1544        )
1545        .bind(chain_id as i32)
1546        .bind(dex.to_string())
1547        .bind(pool_identifier.as_ref())
1548        .fetch_optional(&self.pool)
1549        .await
1550        .map_err(|e| anyhow::anyhow!("Failed to get pool last synced block: {e}"))?;
1551
1552        Ok(result.and_then(|(block_number,)| block_number.map(|b| b as u64)))
1553    }
1554
1555    pub(crate) async fn get_pool_event_sync_state(
1556        &self,
1557        chain_id: u32,
1558        dex: &DexType,
1559        pool_identifier: &PoolIdentifier,
1560    ) -> anyhow::Result<PoolEventSyncState> {
1561        let pool_state = sqlx::query_as::<_, (i32, Option<i64>)>(
1562            "
1563            SELECT event_sync_version, last_full_sync_block_number
1564            FROM pool
1565            WHERE chain_id = $1
1566            AND dex_name = $2
1567            AND pool_identifier = $3
1568            ",
1569        )
1570        .bind(chain_id as i32)
1571        .bind(dex.to_string())
1572        .bind(pool_identifier.as_ref())
1573        .fetch_optional(&self.pool)
1574        .await
1575        .map_err(|e| anyhow::anyhow!("Failed to get pool event sync state: {e}"))?;
1576
1577        let Some((version, last_full_sync_block)) = pool_state else {
1578            return Ok(PoolEventSyncState::default());
1579        };
1580
1581        let family_blocks = sqlx::query_as::<_, (String, i64)>(
1582            "
1583            SELECT event_family, last_full_sync_block_number
1584            FROM pool_event_sync
1585            WHERE chain_id = $1
1586            AND dex_name = $2
1587            AND pool_identifier = $3
1588            ORDER BY event_family
1589            ",
1590        )
1591        .bind(chain_id as i32)
1592        .bind(dex.to_string())
1593        .bind(pool_identifier.as_ref())
1594        .fetch_all(&self.pool)
1595        .await
1596        .map_err(|e| anyhow::anyhow!("Failed to get pool event-family checkpoints: {e}"))?
1597        .into_iter()
1598        .map(|(family, block)| (family, block as u64))
1599        .collect();
1600
1601        Ok(PoolEventSyncState {
1602            version: version as u32,
1603            last_full_sync_block: last_full_sync_block.map(|block| block as u64),
1604            family_blocks,
1605        })
1606    }
1607
1608    pub(crate) async fn update_pool_event_sync(
1609        &self,
1610        chain_id: u32,
1611        dex: &DexType,
1612        pool_identifier: &PoolIdentifier,
1613        event_families: &[&str],
1614        block_number: u64,
1615        version: Option<u32>,
1616    ) -> anyhow::Result<()> {
1617        let mut transaction = self.pool.begin().await?;
1618
1619        for event_family in event_families {
1620            sqlx::query(
1621                "
1622                INSERT INTO pool_event_sync (
1623                    chain_id, dex_name, pool_identifier, event_family,
1624                    last_full_sync_block_number
1625                )
1626                VALUES ($1, $2, $3, $4, $5)
1627                ON CONFLICT (chain_id, dex_name, pool_identifier, event_family)
1628                DO UPDATE SET last_full_sync_block_number =
1629                    GREATEST(
1630                        pool_event_sync.last_full_sync_block_number,
1631                        EXCLUDED.last_full_sync_block_number
1632                    )
1633                ",
1634            )
1635            .bind(chain_id as i32)
1636            .bind(dex.to_string())
1637            .bind(pool_identifier.as_ref())
1638            .bind(event_family)
1639            .bind(block_number as i64)
1640            .execute(&mut *transaction)
1641            .await
1642            .map_err(|e| {
1643                anyhow::anyhow!("Failed to update {event_family} pool event-family checkpoint: {e}")
1644            })?;
1645        }
1646
1647        if let Some(version) = version {
1648            sqlx::query(
1649                "
1650                UPDATE pool
1651                SET
1652                    event_sync_version = GREATEST(event_sync_version, $4),
1653                    last_full_sync_block_number =
1654                        GREATEST(COALESCE(last_full_sync_block_number, $5), $5)
1655                WHERE chain_id = $1
1656                AND dex_name = $2
1657                AND pool_identifier = $3
1658                ",
1659            )
1660            .bind(chain_id as i32)
1661            .bind(dex.to_string())
1662            .bind(pool_identifier.as_ref())
1663            .bind(version as i32)
1664            .bind(block_number as i64)
1665            .execute(&mut *transaction)
1666            .await
1667            .map_err(|e| anyhow::anyhow!("Failed to finalize pool event sync progress: {e}"))?;
1668        }
1669
1670        transaction.commit().await?;
1671        Ok(())
1672    }
1673
1674    /// Retrieves the maximum block number from a specific table for a given pool.
1675    /// This is useful to detect orphaned data where events were inserted but progress wasn't updated.
1676    ///
1677    /// # Errors
1678    ///
1679    /// Returns an error if the database query fails.
1680    pub async fn get_table_last_block(
1681        &self,
1682        chain_id: u32,
1683        table_name: &str,
1684        pool_identifier: &PoolIdentifier,
1685    ) -> anyhow::Result<Option<u64>> {
1686        let query = format!(
1687            "SELECT MAX(block) FROM {table_name} WHERE chain_id = $1 AND pool_identifier = $2"
1688        );
1689        let result = sqlx::query_as::<_, (Option<i64>,)>(AssertSqlSafe(query))
1690            .bind(chain_id as i32)
1691            .bind(pool_identifier.as_ref())
1692            .fetch_optional(&self.pool)
1693            .await
1694            .map_err(|e| anyhow::anyhow!("Failed to get table last block for {table_name}: {e}"))?;
1695
1696        Ok(result.and_then(|(block_number,)| block_number.map(|b| b as u64)))
1697    }
1698
1699    /// Adds a batch of pool fee collect events to the database using batch operations.
1700    ///
1701    /// # Errors
1702    ///
1703    /// Returns an error if the database operation fails.
1704    pub async fn add_pool_collects_batch(
1705        &self,
1706        chain_id: u32,
1707        collects: &[PoolFeeCollect],
1708    ) -> anyhow::Result<()> {
1709        if collects.is_empty() {
1710            return Ok(());
1711        }
1712
1713        // Prepare vectors for each column
1714        let len = collects.len();
1715        let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1716        let mut dex_names: Vec<String> = Vec::with_capacity(len);
1717        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1718        let mut blocks: Vec<i64> = Vec::with_capacity(len);
1719        let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1720        let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1721        let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1722        let mut owners: Vec<String> = Vec::with_capacity(len);
1723        let mut amount0s: Vec<String> = Vec::with_capacity(len);
1724        let mut amount1s: Vec<String> = Vec::with_capacity(len);
1725        let mut tick_lowers: Vec<i32> = Vec::with_capacity(len);
1726        let mut tick_uppers: Vec<i32> = Vec::with_capacity(len);
1727
1728        // Fill vectors from collects
1729        for collect in collects {
1730            chain_ids.push(chain_id as i32);
1731            dex_names.push(collect.dex.name.to_string());
1732            pool_identifiers.push(collect.pool_identifier.to_string());
1733            blocks.push(collect.block as i64);
1734            transaction_hashes.push(collect.transaction_hash.clone());
1735            transaction_indices.push(collect.transaction_index as i32);
1736            log_indices.push(collect.log_index as i32);
1737            owners.push(collect.owner.to_string());
1738            amount0s.push(collect.amount0.to_string());
1739            amount1s.push(collect.amount1.to_string());
1740            tick_lowers.push(collect.tick_lower);
1741            tick_uppers.push(collect.tick_upper);
1742        }
1743
1744        // Execute batch insert with UNNEST
1745        sqlx::query(
1746            "
1747            INSERT INTO pool_collect_event (
1748                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1749                log_index, owner, amount0, amount1, tick_lower, tick_upper
1750            )
1751            SELECT
1752                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1753                log_index, owner, amount0::U256, amount1::U256, tick_lower, tick_upper
1754            FROM UNNEST(
1755                $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
1756                $7::INT[], $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::INT[], $12::INT[]
1757            ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1758                   log_index, owner, amount0, amount1, tick_lower, tick_upper)
1759            ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1760           ",
1761        )
1762        .bind(&chain_ids[..])
1763        .bind(&dex_names[..])
1764        .bind(&pool_identifiers[..])
1765        .bind(&blocks[..])
1766        .bind(&transaction_hashes[..])
1767        .bind(&transaction_indices[..])
1768        .bind(&log_indices[..])
1769        .bind(&owners[..])
1770        .bind(&amount0s[..])
1771        .bind(&amount1s[..])
1772        .bind(&tick_lowers[..])
1773        .bind(&tick_uppers[..])
1774        .execute(&self.pool)
1775        .await
1776        .map(|_| ())
1777        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_fee_collect table: {e}"))
1778    }
1779
1780    /// Inserts multiple pool flash events in a single database operation using UNNEST for optimal performance.
1781    ///
1782    /// # Errors
1783    ///
1784    /// Returns an error if the database operation fails.
1785    pub async fn add_pool_flash_batch(
1786        &self,
1787        chain_id: u32,
1788        flash_events: &[PoolFlash],
1789    ) -> anyhow::Result<()> {
1790        if flash_events.is_empty() {
1791            return Ok(());
1792        }
1793
1794        // Prepare vectors for each column
1795        let len = flash_events.len();
1796        let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1797        let mut dex_names: Vec<String> = Vec::with_capacity(len);
1798        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1799        let mut blocks: Vec<i64> = Vec::with_capacity(len);
1800        let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1801        let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1802        let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1803        let mut senders: Vec<String> = Vec::with_capacity(len);
1804        let mut recipients: Vec<String> = Vec::with_capacity(len);
1805        let mut amount0s: Vec<String> = Vec::with_capacity(len);
1806        let mut amount1s: Vec<String> = Vec::with_capacity(len);
1807        let mut paid0s: Vec<String> = Vec::with_capacity(len);
1808        let mut paid1s: Vec<String> = Vec::with_capacity(len);
1809
1810        // Fill vectors from flash events
1811        for flash in flash_events {
1812            chain_ids.push(chain_id as i32);
1813            dex_names.push(flash.dex.name.to_string());
1814            pool_identifiers.push(flash.pool_identifier.to_string());
1815            blocks.push(flash.block as i64);
1816            transaction_hashes.push(flash.transaction_hash.clone());
1817            transaction_indices.push(flash.transaction_index as i32);
1818            log_indices.push(flash.log_index as i32);
1819            senders.push(flash.sender.to_string());
1820            recipients.push(flash.recipient.to_string());
1821            amount0s.push(flash.amount0.to_string());
1822            amount1s.push(flash.amount1.to_string());
1823            paid0s.push(flash.paid0.to_string());
1824            paid1s.push(flash.paid1.to_string());
1825        }
1826
1827        // Execute batch insert with UNNEST
1828        sqlx::query(
1829            "
1830            INSERT INTO pool_flash_event (
1831                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1832                log_index, sender, recipient, amount0, amount1, paid0, paid1
1833            )
1834            SELECT
1835                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1836                log_index, sender, recipient, amount0::U256, amount1::U256, paid0::U256, paid1::U256
1837            FROM UNNEST(
1838                $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
1839                $7::INT[], $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::TEXT[], $12::TEXT[], $13::TEXT[]
1840            ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1841                   log_index, sender, recipient, amount0, amount1, paid0, paid1)
1842            ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1843           ",
1844        )
1845        .bind(&chain_ids[..])
1846        .bind(&dex_names[..])
1847        .bind(&pool_identifiers[..])
1848        .bind(&blocks[..])
1849        .bind(&transaction_hashes[..])
1850        .bind(&transaction_indices[..])
1851        .bind(&log_indices[..])
1852        .bind(&senders[..])
1853        .bind(&recipients[..])
1854        .bind(&amount0s[..])
1855        .bind(&amount1s[..])
1856        .bind(&paid0s[..])
1857        .bind(&paid1s[..])
1858        .execute(&self.pool)
1859        .await
1860        .map(|_| ())
1861        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_flash_event table: {e}"))
1862    }
1863
1864    /// Inserts multiple pool fee-protocol update events in a single database operation using UNNEST.
1865    ///
1866    /// # Errors
1867    ///
1868    /// Returns an error if the database operation fails.
1869    pub async fn add_pool_fee_protocol_updates_batch(
1870        &self,
1871        chain_id: u32,
1872        updates: &[PoolFeeProtocolUpdate],
1873    ) -> anyhow::Result<()> {
1874        if updates.is_empty() {
1875            return Ok(());
1876        }
1877
1878        // Prepare vectors for each column
1879        let len = updates.len();
1880        let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1881        let mut dex_names: Vec<String> = Vec::with_capacity(len);
1882        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1883        let mut blocks: Vec<i64> = Vec::with_capacity(len);
1884        let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1885        let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1886        let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1887        let mut fee_protocol0s: Vec<i32> = Vec::with_capacity(len);
1888        let mut fee_protocol1s: Vec<i32> = Vec::with_capacity(len);
1889
1890        // Fill vectors from updates
1891        for update in updates {
1892            chain_ids.push(chain_id as i32);
1893            dex_names.push(update.dex.name.to_string());
1894            pool_identifiers.push(update.pool_identifier.to_string());
1895            blocks.push(update.block as i64);
1896            transaction_hashes.push(update.transaction_hash.clone());
1897            transaction_indices.push(update.transaction_index as i32);
1898            log_indices.push(update.log_index as i32);
1899            fee_protocol0s.push(i32::try_from(update.fee_protocol0_new).map_err(|e| {
1900                anyhow::anyhow!(
1901                    "Invalid fee_protocol0_new '{}': {e}",
1902                    update.fee_protocol0_new
1903                )
1904            })?);
1905            fee_protocol1s.push(i32::try_from(update.fee_protocol1_new).map_err(|e| {
1906                anyhow::anyhow!(
1907                    "Invalid fee_protocol1_new '{}': {e}",
1908                    update.fee_protocol1_new
1909                )
1910            })?);
1911        }
1912
1913        // Execute batch insert with UNNEST
1914        sqlx::query(
1915            "
1916            INSERT INTO pool_fee_protocol_update_event (
1917                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1918                log_index, fee_protocol0_new, fee_protocol1_new
1919            )
1920            SELECT
1921                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1922                log_index, fee_protocol0_new, fee_protocol1_new
1923            FROM UNNEST(
1924                $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
1925                $7::INT[], $8::INT[], $9::INT[]
1926            ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1927                   log_index, fee_protocol0_new, fee_protocol1_new)
1928            ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
1929           ",
1930        )
1931        .bind(&chain_ids[..])
1932        .bind(&dex_names[..])
1933        .bind(&pool_identifiers[..])
1934        .bind(&blocks[..])
1935        .bind(&transaction_hashes[..])
1936        .bind(&transaction_indices[..])
1937        .bind(&log_indices[..])
1938        .bind(&fee_protocol0s[..])
1939        .bind(&fee_protocol1s[..])
1940        .execute(&self.pool)
1941        .await
1942        .map(|_| ())
1943        .map_err(|e| {
1944            anyhow::anyhow!("Failed to batch insert into pool_fee_protocol_update_event table: {e}")
1945        })
1946    }
1947
1948    /// Inserts multiple pool protocol-fee withdrawal events in a single database operation using UNNEST.
1949    ///
1950    /// # Errors
1951    ///
1952    /// Returns an error if the database operation fails.
1953    pub async fn add_pool_fee_protocol_collect_batch(
1954        &self,
1955        chain_id: u32,
1956        collects: &[PoolFeeProtocolCollect],
1957    ) -> anyhow::Result<()> {
1958        if collects.is_empty() {
1959            return Ok(());
1960        }
1961
1962        // Prepare vectors for each column
1963        let len = collects.len();
1964        let mut chain_ids: Vec<i32> = Vec::with_capacity(len);
1965        let mut dex_names: Vec<String> = Vec::with_capacity(len);
1966        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
1967        let mut blocks: Vec<i64> = Vec::with_capacity(len);
1968        let mut transaction_hashes: Vec<String> = Vec::with_capacity(len);
1969        let mut transaction_indices: Vec<i32> = Vec::with_capacity(len);
1970        let mut log_indices: Vec<i32> = Vec::with_capacity(len);
1971        let mut senders: Vec<String> = Vec::with_capacity(len);
1972        let mut recipients: Vec<String> = Vec::with_capacity(len);
1973        let mut amount0s: Vec<String> = Vec::with_capacity(len);
1974        let mut amount1s: Vec<String> = Vec::with_capacity(len);
1975
1976        // Fill vectors from collects
1977        for collect in collects {
1978            chain_ids.push(chain_id as i32);
1979            dex_names.push(collect.dex.name.to_string());
1980            pool_identifiers.push(collect.pool_identifier.to_string());
1981            blocks.push(collect.block as i64);
1982            transaction_hashes.push(collect.transaction_hash.clone());
1983            transaction_indices.push(collect.transaction_index as i32);
1984            log_indices.push(collect.log_index as i32);
1985            senders.push(collect.sender.to_string());
1986            recipients.push(collect.recipient.to_string());
1987            amount0s.push(collect.amount0.to_string());
1988            amount1s.push(collect.amount1.to_string());
1989        }
1990
1991        // Execute batch insert with UNNEST
1992        sqlx::query(
1993            "
1994            INSERT INTO pool_fee_protocol_collect_event (
1995                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
1996                log_index, sender, recipient, amount0, amount1
1997            )
1998            SELECT
1999                chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
2000                log_index, sender, recipient, amount0::U256, amount1::U256
2001            FROM UNNEST(
2002                $1::INT[], $2::TEXT[], $3::TEXT[], $4::INT[], $5::TEXT[], $6::INT[],
2003                $7::INT[], $8::TEXT[], $9::TEXT[], $10::TEXT[], $11::TEXT[]
2004            ) AS t(chain_id, dex_name, pool_identifier, block, transaction_hash, transaction_index,
2005                   log_index, sender, recipient, amount0, amount1)
2006            ON CONFLICT (chain_id, transaction_hash, log_index) DO NOTHING
2007           ",
2008        )
2009        .bind(&chain_ids[..])
2010        .bind(&dex_names[..])
2011        .bind(&pool_identifiers[..])
2012        .bind(&blocks[..])
2013        .bind(&transaction_hashes[..])
2014        .bind(&transaction_indices[..])
2015        .bind(&log_indices[..])
2016        .bind(&senders[..])
2017        .bind(&recipients[..])
2018        .bind(&amount0s[..])
2019        .bind(&amount1s[..])
2020        .execute(&self.pool)
2021        .await
2022        .map(|_| ())
2023        .map_err(|e| {
2024            anyhow::anyhow!(
2025                "Failed to batch insert into pool_fee_protocol_collect_event table: {e}"
2026            )
2027        })
2028    }
2029
2030    /// Adds a pool snapshot to the database.
2031    ///
2032    /// # Errors
2033    ///
2034    /// Returns an error if the database insert fails.
2035    pub async fn add_pool_snapshot(
2036        &self,
2037        chain_id: u32,
2038        dex_name: &DexType,
2039        pool_identifier: &PoolIdentifier,
2040        snapshot: &PoolSnapshot,
2041    ) -> anyhow::Result<()> {
2042        sqlx::query(
2043            "
2044            INSERT INTO pool_snapshot (
2045                chain_id, dex_name, pool_identifier, block, transaction_index, log_index, transaction_hash,
2046                current_tick, price_sqrt_ratio_x96, liquidity,
2047                protocol_fees_token0, protocol_fees_token1, fee_protocol,
2048                fee_protocol0_basis_points, fee_protocol1_basis_points,
2049                fee_growth_global_0, fee_growth_global_1,
2050                total_amount0_deposited, total_amount1_deposited,
2051                total_amount0_collected, total_amount1_collected,
2052                total_swaps, total_mints, total_burns, total_fee_collects, total_flashes,
2053                liquidity_utilization_rate
2054            ) VALUES (
2055                $1, $2, $3, $4, $5, $6, $7,
2056                $8, $9::U160, $10::U128, $11::U256, $12::U256, $13, $14, $15,
2057                $16::U256, $17::U256, $18::U256, $19::U256, $20::U256, $21::U256,
2058                $22, $23, $24, $25, $26, $27
2059            )
2060            ON CONFLICT (chain_id, pool_identifier, block, transaction_index, log_index)
2061            DO UPDATE SET
2062                dex_name = EXCLUDED.dex_name,
2063                transaction_hash = EXCLUDED.transaction_hash,
2064                current_tick = EXCLUDED.current_tick,
2065                price_sqrt_ratio_x96 = EXCLUDED.price_sqrt_ratio_x96,
2066                liquidity = EXCLUDED.liquidity,
2067                protocol_fees_token0 = EXCLUDED.protocol_fees_token0,
2068                protocol_fees_token1 = EXCLUDED.protocol_fees_token1,
2069                fee_protocol = EXCLUDED.fee_protocol,
2070                fee_protocol0_basis_points = EXCLUDED.fee_protocol0_basis_points,
2071                fee_protocol1_basis_points = EXCLUDED.fee_protocol1_basis_points,
2072                fee_growth_global_0 = EXCLUDED.fee_growth_global_0,
2073                fee_growth_global_1 = EXCLUDED.fee_growth_global_1,
2074                total_amount0_deposited = EXCLUDED.total_amount0_deposited,
2075                total_amount1_deposited = EXCLUDED.total_amount1_deposited,
2076                total_amount0_collected = EXCLUDED.total_amount0_collected,
2077                total_amount1_collected = EXCLUDED.total_amount1_collected,
2078                total_swaps = EXCLUDED.total_swaps,
2079                total_mints = EXCLUDED.total_mints,
2080                total_burns = EXCLUDED.total_burns,
2081                total_fee_collects = EXCLUDED.total_fee_collects,
2082                total_flashes = EXCLUDED.total_flashes,
2083                liquidity_utilization_rate = EXCLUDED.liquidity_utilization_rate
2084            ",
2085        )
2086        .bind(chain_id as i32)
2087        .bind(dex_name.to_string())
2088        .bind(pool_identifier.as_ref())
2089        .bind(snapshot.block_position.number as i64)
2090        .bind(snapshot.block_position.transaction_index as i32)
2091        .bind(snapshot.block_position.log_index as i32)
2092        .bind(snapshot.block_position.transaction_hash.clone())
2093        .bind(snapshot.state.current_tick)
2094        .bind(snapshot.state.price_sqrt_ratio_x96.to_string())
2095        .bind(snapshot.state.liquidity.to_string())
2096        .bind(snapshot.state.protocol_fees_token0.to_string())
2097        .bind(snapshot.state.protocol_fees_token1.to_string())
2098        .bind(snapshot.state.fee_protocol as i16)
2099        .bind(snapshot.state.fee_protocol0_basis_points.map(|v| v as i32))
2100        .bind(snapshot.state.fee_protocol1_basis_points.map(|v| v as i32))
2101        .bind(snapshot.state.fee_growth_global_0.to_string())
2102        .bind(snapshot.state.fee_growth_global_1.to_string())
2103        .bind(snapshot.analytics.total_amount0_deposited.to_string())
2104        .bind(snapshot.analytics.total_amount1_deposited.to_string())
2105        .bind(snapshot.analytics.total_amount0_collected.to_string())
2106        .bind(snapshot.analytics.total_amount1_collected.to_string())
2107        .bind(snapshot.analytics.total_swaps as i32)
2108        .bind(snapshot.analytics.total_mints as i32)
2109        .bind(snapshot.analytics.total_burns as i32)
2110        .bind(snapshot.analytics.total_fee_collects as i32)
2111        .bind(snapshot.analytics.total_flashes as i32)
2112        .bind(snapshot.analytics.liquidity_utilization_rate)
2113        .execute(&self.pool)
2114        .await
2115        .map(|_| ())
2116        .map_err(|e| anyhow::anyhow!("Failed to insert into pool_snapshot table: {e}"))
2117    }
2118
2119    /// Inserts multiple pool positions in a single database operation using UNNEST for optimal performance.
2120    ///
2121    /// # Errors
2122    ///
2123    /// Returns an error if the database operation fails.
2124    pub async fn add_pool_positions_batch(
2125        &self,
2126        chain_id: u32,
2127        snapshot_block: u64,
2128        snapshot_transaction_index: u32,
2129        snapshot_log_index: u32,
2130        positions: &[(PoolIdentifier, PoolPosition)],
2131    ) -> anyhow::Result<()> {
2132        if positions.is_empty() {
2133            return Ok(());
2134        }
2135
2136        // Prepare vectors for each column
2137        let len = positions.len();
2138        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
2139        let mut owners: Vec<String> = Vec::with_capacity(len);
2140        let mut tick_lowers: Vec<i32> = Vec::with_capacity(len);
2141        let mut tick_uppers: Vec<i32> = Vec::with_capacity(len);
2142        let mut liquidities: Vec<String> = Vec::with_capacity(len);
2143        let mut fee_growth_inside_0_lasts: Vec<String> = Vec::with_capacity(len);
2144        let mut fee_growth_inside_1_lasts: Vec<String> = Vec::with_capacity(len);
2145        let mut tokens_owed_0s: Vec<String> = Vec::with_capacity(len);
2146        let mut tokens_owed_1s: Vec<String> = Vec::with_capacity(len);
2147        let mut total_amount0_depositeds: Vec<Option<String>> = Vec::with_capacity(len);
2148        let mut total_amount1_depositeds: Vec<Option<String>> = Vec::with_capacity(len);
2149        let mut total_amount0_collecteds: Vec<Option<String>> = Vec::with_capacity(len);
2150        let mut total_amount1_collecteds: Vec<Option<String>> = Vec::with_capacity(len);
2151
2152        // Fill vectors from positions
2153        for (pool_address, position) in positions {
2154            pool_identifiers.push(pool_address.to_string());
2155            owners.push(position.owner.to_string());
2156            tick_lowers.push(position.tick_lower);
2157            tick_uppers.push(position.tick_upper);
2158            liquidities.push(position.liquidity.to_string());
2159            fee_growth_inside_0_lasts.push(position.fee_growth_inside_0_last.to_string());
2160            fee_growth_inside_1_lasts.push(position.fee_growth_inside_1_last.to_string());
2161            tokens_owed_0s.push(position.tokens_owed_0.to_string());
2162            tokens_owed_1s.push(position.tokens_owed_1.to_string());
2163            total_amount0_depositeds.push(Some(position.total_amount0_deposited.to_string()));
2164            total_amount1_depositeds.push(Some(position.total_amount1_deposited.to_string()));
2165            total_amount0_collecteds.push(Some(position.total_amount0_collected.to_string()));
2166            total_amount1_collecteds.push(Some(position.total_amount1_collected.to_string()));
2167        }
2168
2169        // Execute batch insert with UNNEST
2170        sqlx::query(
2171            "
2172            INSERT INTO pool_position (
2173                chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index,
2174                owner, tick_lower, tick_upper,
2175                liquidity, fee_growth_inside_0_last, fee_growth_inside_1_last,
2176                tokens_owed_0, tokens_owed_1,
2177                total_amount0_deposited, total_amount1_deposited,
2178                total_amount0_collected, total_amount1_collected
2179            )
2180            SELECT
2181                $1, pool_identifier, $2, $3, $4,
2182                owner, tick_lower, tick_upper,
2183                liquidity::U128, fee_growth_inside_0_last::U256, fee_growth_inside_1_last::U256,
2184                tokens_owed_0::U128, tokens_owed_1::U128,
2185                total_amount0_deposited::U256, total_amount1_deposited::U256,
2186                total_amount0_collected::U128, total_amount1_collected::U128
2187            FROM UNNEST(
2188                $5::TEXT[], $6::TEXT[], $7::INT[], $8::INT[], $9::TEXT[], $10::TEXT[],
2189                $11::TEXT[], $12::TEXT[], $13::TEXT[], $14::TEXT[], $15::TEXT[],
2190                $16::TEXT[], $17::TEXT[]
2191            ) AS t(pool_identifier, owner, tick_lower, tick_upper,
2192                   liquidity, fee_growth_inside_0_last, fee_growth_inside_1_last,
2193                   tokens_owed_0, tokens_owed_1,
2194                   total_amount0_deposited, total_amount1_deposited,
2195                   total_amount0_collected, total_amount1_collected)
2196            ON CONFLICT (chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index, owner, tick_lower, tick_upper)
2197            DO UPDATE SET
2198                liquidity = EXCLUDED.liquidity,
2199                fee_growth_inside_0_last = EXCLUDED.fee_growth_inside_0_last,
2200                fee_growth_inside_1_last = EXCLUDED.fee_growth_inside_1_last,
2201                tokens_owed_0 = EXCLUDED.tokens_owed_0,
2202                tokens_owed_1 = EXCLUDED.tokens_owed_1,
2203                total_amount0_deposited = EXCLUDED.total_amount0_deposited,
2204                total_amount1_deposited = EXCLUDED.total_amount1_deposited,
2205                total_amount0_collected = EXCLUDED.total_amount0_collected,
2206                total_amount1_collected = EXCLUDED.total_amount1_collected
2207           ",
2208        )
2209        .bind(chain_id as i32)
2210        .bind(snapshot_block as i64)
2211        .bind(snapshot_transaction_index as i32)
2212        .bind(snapshot_log_index as i32)
2213        .bind(&pool_identifiers[..])
2214        .bind(&owners[..])
2215        .bind(&tick_lowers[..])
2216        .bind(&tick_uppers[..])
2217        .bind(&liquidities[..])
2218        .bind(&fee_growth_inside_0_lasts[..])
2219        .bind(&fee_growth_inside_1_lasts[..])
2220        .bind(&tokens_owed_0s[..])
2221        .bind(&tokens_owed_1s[..])
2222        .bind(&total_amount0_depositeds as &[Option<String>])
2223        .bind(&total_amount1_depositeds as &[Option<String>])
2224        .bind(&total_amount0_collecteds as &[Option<String>])
2225        .bind(&total_amount1_collecteds as &[Option<String>])
2226        .execute(&self.pool)
2227        .await
2228        .map(|_| ())
2229        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_position table: {e}"))
2230    }
2231
2232    /// Inserts multiple pool ticks in a single database operation using UNNEST for optimal performance.
2233    ///
2234    /// # Errors
2235    ///
2236    /// Returns an error if the database operation fails.
2237    pub async fn add_pool_ticks_batch(
2238        &self,
2239        chain_id: u32,
2240        snapshot_block: u64,
2241        snapshot_transaction_index: u32,
2242        snapshot_log_index: u32,
2243        ticks: &[(PoolIdentifier, &PoolTick)],
2244    ) -> anyhow::Result<()> {
2245        if ticks.is_empty() {
2246            return Ok(());
2247        }
2248
2249        // Prepare vectors for each column
2250        let len = ticks.len();
2251        let mut pool_identifiers: Vec<String> = Vec::with_capacity(len);
2252        let mut tick_values: Vec<i32> = Vec::with_capacity(len);
2253        let mut liquidity_grosses: Vec<String> = Vec::with_capacity(len);
2254        let mut liquidity_nets: Vec<String> = Vec::with_capacity(len);
2255        let mut fee_growth_outside_0s: Vec<String> = Vec::with_capacity(len);
2256        let mut fee_growth_outside_1s: Vec<String> = Vec::with_capacity(len);
2257        let mut initializeds: Vec<bool> = Vec::with_capacity(len);
2258        let mut last_updated_blocks: Vec<i64> = Vec::with_capacity(len);
2259
2260        // Fill vectors from ticks
2261        for (pool_address, tick) in ticks {
2262            pool_identifiers.push(pool_address.to_string());
2263            tick_values.push(tick.value);
2264            liquidity_grosses.push(tick.liquidity_gross.to_string());
2265            liquidity_nets.push(tick.liquidity_net.to_string());
2266            fee_growth_outside_0s.push(tick.fee_growth_outside_0.to_string());
2267            fee_growth_outside_1s.push(tick.fee_growth_outside_1.to_string());
2268            initializeds.push(tick.initialized);
2269            last_updated_blocks.push(tick.last_updated_block as i64);
2270        }
2271
2272        // Execute batch insert with UNNEST
2273        sqlx::query(
2274            "
2275            INSERT INTO pool_tick (
2276                chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index,
2277                tick_value, liquidity_gross, liquidity_net,
2278                fee_growth_outside_0, fee_growth_outside_1, initialized, last_updated_block
2279            )
2280            SELECT
2281                $1, pool_identifier, $2, $3, $4,
2282                tick_value, liquidity_gross::U128, liquidity_net::I128,
2283                fee_growth_outside_0::U256, fee_growth_outside_1::U256, initialized, last_updated_block
2284            FROM UNNEST(
2285                $5::TEXT[], $6::INT[], $7::TEXT[], $8::TEXT[], $9::TEXT[],
2286                $10::TEXT[], $11::BOOLEAN[], $12::BIGINT[]
2287            ) AS t(pool_identifier, tick_value, liquidity_gross, liquidity_net,
2288                   fee_growth_outside_0, fee_growth_outside_1, initialized, last_updated_block)
2289            ON CONFLICT (chain_id, pool_identifier, snapshot_block, snapshot_transaction_index, snapshot_log_index, tick_value)
2290            DO UPDATE SET
2291                liquidity_gross = EXCLUDED.liquidity_gross,
2292                liquidity_net = EXCLUDED.liquidity_net,
2293                fee_growth_outside_0 = EXCLUDED.fee_growth_outside_0,
2294                fee_growth_outside_1 = EXCLUDED.fee_growth_outside_1,
2295                initialized = EXCLUDED.initialized,
2296                last_updated_block = EXCLUDED.last_updated_block
2297           ",
2298        )
2299        .bind(chain_id as i32)
2300        .bind(snapshot_block as i64)
2301        .bind(snapshot_transaction_index as i32)
2302        .bind(snapshot_log_index as i32)
2303        .bind(&pool_identifiers[..])
2304        .bind(&tick_values[..])
2305        .bind(&liquidity_grosses[..])
2306        .bind(&liquidity_nets[..])
2307        .bind(&fee_growth_outside_0s[..])
2308        .bind(&fee_growth_outside_1s[..])
2309        .bind(&initializeds[..])
2310        .bind(&last_updated_blocks[..])
2311        .execute(&self.pool)
2312        .await
2313        .map(|_| ())
2314        .map_err(|e| anyhow::anyhow!("Failed to batch insert into pool_tick table: {e}"))
2315    }
2316
2317    /// Updates the initial price and tick for a pool.
2318    ///
2319    /// # Errors
2320    ///
2321    /// Returns an error if the database update fails.
2322    pub async fn update_pool_initial_price_tick(
2323        &self,
2324        chain_id: u32,
2325        initialize_event: &InitializeEvent,
2326    ) -> anyhow::Result<()> {
2327        sqlx::query(
2328            "
2329            UPDATE pool
2330            SET
2331                initial_tick = $4,
2332                initial_sqrt_price_x96 = $5
2333            WHERE chain_id = $1
2334            AND dex_name = $2
2335            AND pool_identifier = $3
2336            ",
2337        )
2338        .bind(chain_id as i32)
2339        .bind(initialize_event.dex.name.to_string())
2340        .bind(initialize_event.pool_identifier.as_ref())
2341        .bind(initialize_event.tick)
2342        .bind(initialize_event.sqrt_price_x96.to_string())
2343        .execute(&self.pool)
2344        .await
2345        .map(|_| ())
2346        .map_err(|e| anyhow::anyhow!("Failed to update pool initial price and tick: {e}"))
2347    }
2348
2349    /// Loads the latest usable pool snapshot from the database.
2350    ///
2351    /// Returns the most recent snapshot usable as a replay start point: on-chain validated or
2352    /// replay-derived. Snapshots that failed on-chain validation are excluded.
2353    ///
2354    /// # Errors
2355    ///
2356    /// Returns an error if the database query fails.
2357    pub async fn load_latest_valid_pool_snapshot(
2358        &self,
2359        chain_id: u32,
2360        pool_identifier: &PoolIdentifier,
2361    ) -> anyhow::Result<Option<PoolSnapshot>> {
2362        self.load_latest_pool_snapshot(chain_id, pool_identifier, None, true)
2363            .await
2364    }
2365
2366    /// Loads the latest pool snapshot from the database, optionally bounded by block.
2367    ///
2368    /// When `max_block` is `Some`, only snapshots at or before that block are considered, so a
2369    /// backtest can restore pool state as of a replay start. When `require_valid` is `true`,
2370    /// snapshots that failed on-chain validation are excluded (both on-chain validated and
2371    /// replay-derived snapshots are returned).
2372    ///
2373    /// # Errors
2374    ///
2375    /// Returns an error if the database query fails.
2376    pub async fn load_latest_pool_snapshot(
2377        &self,
2378        chain_id: u32,
2379        pool_identifier: &PoolIdentifier,
2380        max_block: Option<u64>,
2381        require_valid: bool,
2382    ) -> anyhow::Result<Option<PoolSnapshot>> {
2383        let allow_invalid = !require_valid;
2384        let result = sqlx::query(
2385            "
2386            SELECT
2387                block, transaction_index, log_index, transaction_hash,
2388                COALESCE(
2389                    (SELECT hash FROM pool_event_block WHERE pool_event_block.chain_id = pool_snapshot.chain_id AND pool_event_block.number = pool_snapshot.block),
2390                    (SELECT hash FROM block WHERE block.chain_id = pool_snapshot.chain_id AND block.number = pool_snapshot.block)
2391                ) as block_hash,
2392                current_tick, price_sqrt_ratio_x96::TEXT, liquidity::TEXT,
2393                protocol_fees_token0::TEXT, protocol_fees_token1::TEXT, fee_protocol,
2394                fee_protocol0_basis_points, fee_protocol1_basis_points,
2395                fee_growth_global_0::TEXT, fee_growth_global_1::TEXT,
2396                total_amount0_deposited::TEXT, total_amount1_deposited::TEXT,
2397                total_amount0_collected::TEXT, total_amount1_collected::TEXT,
2398                total_swaps, total_mints, total_burns, total_fee_collects, total_flashes,
2399                liquidity_utilization_rate,
2400                (SELECT dex_name FROM pool WHERE chain_id = $1 AND address = $2) as dex_name,
2401                COALESCE(
2402                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_snapshot.chain_id AND block.number = pool_snapshot.block),
2403                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_snapshot.chain_id AND pool_event_block.number = pool_snapshot.block)
2404                ) as block_timestamp
2405            FROM pool_snapshot
2406            WHERE chain_id = $1 AND pool_identifier = $2
2407                AND ($3::BIGINT IS NULL OR block <= $3)
2408                AND ($4 OR validation_state <> 'invalid')
2409            ORDER BY block DESC, transaction_index DESC, log_index DESC
2410            LIMIT 1
2411            ",
2412        )
2413        .bind(chain_id as i32)
2414        .bind(pool_identifier.as_ref())
2415        .bind(max_block.map(|b| b as i64))
2416        .bind(allow_invalid)
2417        .fetch_optional(&self.pool)
2418        .await
2419        .map_err(|e| anyhow::anyhow!("Failed to load latest valid pool snapshot: {e}"))?;
2420
2421        if let Some(row) = result {
2422            // Parse snapshot state
2423            let block: i64 = row.get("block");
2424            let transaction_index: i32 = row.get("transaction_index");
2425            let log_index: i32 = row.get("log_index");
2426            let transaction_hash: String = row.get("transaction_hash");
2427            let observed_block_hash = row.try_get::<Option<String>, _>("block_hash")?;
2428            let block =
2429                u64::try_from(block).with_context(|| "Pool snapshot block number is negative")?;
2430            let transaction_index = u32::try_from(transaction_index)
2431                .with_context(|| "Pool snapshot transaction index is negative")?;
2432            let log_index =
2433                u32::try_from(log_index).with_context(|| "Pool snapshot log index is negative")?;
2434            let block_hash = if transaction_index == BLOCK_SCOPED_SNAPSHOT_INDEX
2435                && log_index == BLOCK_SCOPED_SNAPSHOT_INDEX
2436            {
2437                Some(transaction_hash.clone())
2438            } else {
2439                observed_block_hash
2440            };
2441            let block_timestamp = row
2442                .try_get::<Option<String>, _>("block_timestamp")?
2443                .ok_or_else(|| {
2444                    anyhow::anyhow!(
2445                        "Missing block timestamp for pool snapshot {} at block {}",
2446                        pool_identifier,
2447                        block
2448                    )
2449                })?;
2450            let timestamp = parse_cached_block_timestamp(&block_timestamp)
2451                .map_err(|e| anyhow::anyhow!("Invalid block timestamp '{block_timestamp}': {e}"))?;
2452
2453            let block_position =
2454                BlockPosition::new(block, transaction_hash, transaction_index, log_index)
2455                    .with_block_hash(block_hash);
2456
2457            let fee_protocol_value = row.get::<i16, _>("fee_protocol");
2458            let fee_protocol = u8::try_from(fee_protocol_value).with_context(|| {
2459                format!("Invalid pool snapshot fee protocol {fee_protocol_value}")
2460            })?;
2461            let fee_protocol0_basis_points = row
2462                .get::<Option<i32>, _>("fee_protocol0_basis_points")
2463                .map(|value| {
2464                    u32::try_from(value).with_context(|| {
2465                        format!("Invalid token0 fee protocol basis points {value}")
2466                    })
2467                })
2468                .transpose()?;
2469            let fee_protocol1_basis_points = row
2470                .get::<Option<i32>, _>("fee_protocol1_basis_points")
2471                .map(|value| {
2472                    u32::try_from(value).with_context(|| {
2473                        format!("Invalid token1 fee protocol basis points {value}")
2474                    })
2475                })
2476                .transpose()?;
2477
2478            let state = PoolState {
2479                current_tick: row.get("current_tick"),
2480                price_sqrt_ratio_x96: row.get::<String, _>("price_sqrt_ratio_x96").parse()?,
2481                liquidity: row.get::<String, _>("liquidity").parse()?,
2482                protocol_fees_token0: row.get::<String, _>("protocol_fees_token0").parse()?,
2483                protocol_fees_token1: row.get::<String, _>("protocol_fees_token1").parse()?,
2484                fee_protocol,
2485                fee_protocol0_basis_points,
2486                fee_protocol1_basis_points,
2487                fee_growth_global_0: row.get::<String, _>("fee_growth_global_0").parse()?,
2488                fee_growth_global_1: row.get::<String, _>("fee_growth_global_1").parse()?,
2489            };
2490
2491            let analytics = PoolAnalytics {
2492                total_amount0_deposited: row.get::<String, _>("total_amount0_deposited").parse()?,
2493                total_amount1_deposited: row.get::<String, _>("total_amount1_deposited").parse()?,
2494                total_amount0_collected: row.get::<String, _>("total_amount0_collected").parse()?,
2495                total_amount1_collected: row.get::<String, _>("total_amount1_collected").parse()?,
2496                total_swaps: u64::try_from(row.get::<i32, _>("total_swaps"))
2497                    .with_context(|| "Pool snapshot total swaps is negative")?,
2498                total_mints: u64::try_from(row.get::<i32, _>("total_mints"))
2499                    .with_context(|| "Pool snapshot total mints is negative")?,
2500                total_burns: u64::try_from(row.get::<i32, _>("total_burns"))
2501                    .with_context(|| "Pool snapshot total burns is negative")?,
2502                total_fee_collects: u64::try_from(row.get::<i32, _>("total_fee_collects"))
2503                    .with_context(|| "Pool snapshot total fee collects is negative")?,
2504                total_flashes: u64::try_from(row.get::<i32, _>("total_flashes"))
2505                    .with_context(|| "Pool snapshot total flashes is negative")?,
2506                liquidity_utilization_rate: row.get::<f64, _>("liquidity_utilization_rate"),
2507            };
2508
2509            // Load positions and ticks
2510            let positions = self
2511                .load_pool_positions_for_snapshot(
2512                    chain_id,
2513                    pool_identifier,
2514                    block,
2515                    transaction_index,
2516                    log_index,
2517                )
2518                .await?;
2519
2520            let ticks = self
2521                .load_pool_ticks_for_snapshot(
2522                    chain_id,
2523                    pool_identifier,
2524                    block,
2525                    transaction_index,
2526                    log_index,
2527                )
2528                .await?;
2529
2530            let dex_name = row
2531                .try_get::<Option<String>, _>("dex_name")?
2532                .ok_or_else(|| {
2533                    anyhow::anyhow!("Missing dex_name for pool snapshot {}", pool_identifier)
2534                })?;
2535            let chain = Chain::from_chain_id(chain_id)
2536                .ok_or_else(|| anyhow::anyhow!("Unknown chain_id: {chain_id}"))?;
2537
2538            let dex_type = DexType::from_dex_name(&dex_name)
2539                .ok_or_else(|| anyhow::anyhow!("Unknown dex_name: {dex_name}"))?;
2540
2541            let dex_extended = crate::exchanges::get_dex_extended(chain.name, &dex_type)
2542                .ok_or_else(|| {
2543                    anyhow::anyhow!("No DEX extended found for {} on {}", dex_name, chain.name)
2544                })?;
2545
2546            let instrument_id =
2547                Pool::create_instrument_id(chain.name, &dex_extended.dex, pool_identifier.as_ref());
2548
2549            Ok(Some(PoolSnapshot::new(
2550                instrument_id,
2551                state,
2552                positions,
2553                ticks,
2554                analytics,
2555                block_position,
2556                timestamp, // ts_event
2557                timestamp, // ts_init (same block timestamp)
2558            )))
2559        } else {
2560            Ok(None)
2561        }
2562    }
2563
2564    /// Sets the validation state of a pool snapshot after a validation attempt.
2565    ///
2566    /// `state` is one of `on_chain` (hydrated and matched), `replay` (replay-derived, not checked),
2567    /// or `invalid` (hydrated and mismatched).
2568    ///
2569    /// # Errors
2570    ///
2571    /// Returns an error if the database operation fails.
2572    pub async fn set_pool_snapshot_validation_state(
2573        &self,
2574        chain_id: u32,
2575        pool_identifier: &PoolIdentifier,
2576        block: u64,
2577        transaction_index: u32,
2578        log_index: u32,
2579        state: &str,
2580    ) -> anyhow::Result<()> {
2581        sqlx::query(
2582            "
2583            UPDATE pool_snapshot
2584            SET validation_state = $6
2585            WHERE chain_id = $1
2586            AND pool_identifier = $2
2587            AND block = $3
2588            AND transaction_index = $4
2589            AND log_index = $5
2590            ",
2591        )
2592        .bind(chain_id as i32)
2593        .bind(pool_identifier.as_ref())
2594        .bind(block as i64)
2595        .bind(transaction_index as i32)
2596        .bind(log_index as i32)
2597        .bind(state)
2598        .execute(&self.pool)
2599        .await
2600        .map(|_| ())
2601        .map_err(|e| anyhow::anyhow!("Failed to set pool snapshot validation state: {e}"))
2602    }
2603
2604    /// Reads the stored `validation_state` for the snapshot at the given watermark.
2605    ///
2606    /// Returns `None` when no snapshot row exists at that position. Used to report the persisted
2607    /// verdict (rather than re-deriving `replay`) when on-chain validation cannot reach the block.
2608    ///
2609    /// # Errors
2610    ///
2611    /// Returns an error if the database query fails.
2612    pub async fn get_pool_snapshot_validation_state(
2613        &self,
2614        chain_id: u32,
2615        pool_identifier: &PoolIdentifier,
2616        block: u64,
2617        transaction_index: u32,
2618        log_index: u32,
2619    ) -> anyhow::Result<Option<String>> {
2620        let row = sqlx::query(
2621            "
2622            SELECT validation_state
2623            FROM pool_snapshot
2624            WHERE chain_id = $1
2625            AND pool_identifier = $2
2626            AND block = $3
2627            AND transaction_index = $4
2628            AND log_index = $5
2629            ",
2630        )
2631        .bind(chain_id as i32)
2632        .bind(pool_identifier.as_ref())
2633        .bind(block as i64)
2634        .bind(transaction_index as i32)
2635        .bind(log_index as i32)
2636        .fetch_optional(&self.pool)
2637        .await
2638        .map_err(|e| anyhow::anyhow!("Failed to get pool snapshot validation state: {e}"))?;
2639
2640        Ok(row.map(|row| row.get::<String, _>("validation_state")))
2641    }
2642
2643    /// Loads all positions for a specific snapshot.
2644    ///
2645    /// # Errors
2646    ///
2647    /// Returns an error if the database query fails.
2648    pub async fn load_pool_positions_for_snapshot(
2649        &self,
2650        chain_id: u32,
2651        pool_identifier: &PoolIdentifier,
2652        snapshot_block: u64,
2653        snapshot_transaction_index: u32,
2654        snapshot_log_index: u32,
2655    ) -> anyhow::Result<Vec<PoolPosition>> {
2656        let rows = sqlx::query(
2657            "
2658            SELECT
2659                owner, tick_lower, tick_upper,
2660                liquidity::TEXT, fee_growth_inside_0_last::TEXT, fee_growth_inside_1_last::TEXT,
2661                tokens_owed_0::TEXT, tokens_owed_1::TEXT,
2662                total_amount0_deposited::TEXT, total_amount1_deposited::TEXT,
2663                total_amount0_collected::TEXT, total_amount1_collected::TEXT
2664            FROM pool_position
2665            WHERE chain_id = $1
2666            AND pool_identifier = $2
2667            AND snapshot_block = $3
2668            AND snapshot_transaction_index = $4
2669            AND snapshot_log_index = $5
2670            ",
2671        )
2672        .bind(chain_id as i32)
2673        .bind(pool_identifier.as_ref())
2674        .bind(snapshot_block as i64)
2675        .bind(snapshot_transaction_index as i32)
2676        .bind(snapshot_log_index as i32)
2677        .fetch_all(&self.pool)
2678        .await
2679        .map_err(|e| anyhow::anyhow!("Failed to load pool positions: {e}"))?;
2680
2681        rows.iter()
2682            .map(|row| {
2683                let owner: String = row.get("owner");
2684                let position = PoolPosition {
2685                    owner: validate_address(&owner)?,
2686                    tick_lower: row.get("tick_lower"),
2687                    tick_upper: row.get("tick_upper"),
2688                    liquidity: row.get::<String, _>("liquidity").parse()?,
2689                    fee_growth_inside_0_last: row
2690                        .get::<String, _>("fee_growth_inside_0_last")
2691                        .parse()?,
2692                    fee_growth_inside_1_last: row
2693                        .get::<String, _>("fee_growth_inside_1_last")
2694                        .parse()?,
2695                    tokens_owed_0: row.get::<String, _>("tokens_owed_0").parse()?,
2696                    tokens_owed_1: row.get::<String, _>("tokens_owed_1").parse()?,
2697                    total_amount0_deposited: row
2698                        .get::<String, _>("total_amount0_deposited")
2699                        .parse()?,
2700                    total_amount1_deposited: row
2701                        .get::<String, _>("total_amount1_deposited")
2702                        .parse()?,
2703                    total_amount0_collected: row
2704                        .get::<String, _>("total_amount0_collected")
2705                        .parse()?,
2706                    total_amount1_collected: row
2707                        .get::<String, _>("total_amount1_collected")
2708                        .parse()?,
2709                };
2710                Ok(position)
2711            })
2712            .collect()
2713    }
2714
2715    /// Loads all ticks for a specific snapshot.
2716    ///
2717    /// # Errors
2718    ///
2719    /// Returns an error if the database query fails.
2720    pub async fn load_pool_ticks_for_snapshot(
2721        &self,
2722        chain_id: u32,
2723        pool_identifier: &PoolIdentifier,
2724        snapshot_block: u64,
2725        snapshot_transaction_index: u32,
2726        snapshot_log_index: u32,
2727    ) -> anyhow::Result<Vec<PoolTick>> {
2728        let rows = sqlx::query(
2729            "
2730            SELECT
2731                tick_value, liquidity_gross::TEXT, liquidity_net::TEXT,
2732                fee_growth_outside_0::TEXT, fee_growth_outside_1::TEXT, initialized,
2733                last_updated_block
2734            FROM pool_tick
2735            WHERE chain_id = $1
2736            AND pool_identifier = $2
2737            AND snapshot_block = $3
2738            AND snapshot_transaction_index = $4
2739            AND snapshot_log_index = $5
2740            ",
2741        )
2742        .bind(chain_id as i32)
2743        .bind(pool_identifier.as_ref())
2744        .bind(snapshot_block as i64)
2745        .bind(snapshot_transaction_index as i32)
2746        .bind(snapshot_log_index as i32)
2747        .fetch_all(&self.pool)
2748        .await
2749        .map_err(|e| anyhow::anyhow!("Failed to load pool ticks: {e}"))?;
2750
2751        rows.iter()
2752            .map(|row| {
2753                let tick = PoolTick::new(
2754                    row.get("tick_value"),
2755                    row.get::<String, _>("liquidity_gross").parse()?,
2756                    row.get::<String, _>("liquidity_net").parse()?,
2757                    row.get::<String, _>("fee_growth_outside_0").parse()?,
2758                    row.get::<String, _>("fee_growth_outside_1").parse()?,
2759                    row.get("initialized"),
2760                    u64::try_from(row.get::<i64, _>("last_updated_block"))
2761                        .with_context(|| "Pool tick last updated block is negative")?,
2762                );
2763                Ok(tick)
2764            })
2765            .collect()
2766    }
2767
2768    /// Streams pool events from all event tables (swap, liquidity, collect) for a specific pool.
2769    ///
2770    /// Creates a unified stream of pool events from multiple tables, ordering them chronologically
2771    /// by block number, transaction index, and log index. Optionally resumes from a specific
2772    /// block position and stops at a maximum block.
2773    ///
2774    /// # Returns
2775    ///
2776    /// A stream of `DexPoolData` events in chronological order.
2777    ///
2778    /// # Errors
2779    ///
2780    /// Returns an error if the database query fails or if event transformation fails.
2781    pub fn stream_pool_events<'a>(
2782        &'a self,
2783        chain: SharedChain,
2784        dex: SharedDex,
2785        instrument_id: InstrumentId,
2786        pool_identifier: PoolIdentifier,
2787        from_position: Option<BlockPosition>,
2788        to_block: Option<u64>,
2789    ) -> Pin<Box<dyn Stream<Item = Result<DexPoolData, anyhow::Error>> + Send + 'a>> {
2790        const QUERY_ALL: &str = "
2791            SELECT events.*,
2792                COALESCE(
2793                    (SELECT hash FROM pool_event_block WHERE pool_event_block.chain_id = events.chain_id AND pool_event_block.number = events.block),
2794                    (SELECT hash FROM block WHERE block.chain_id = events.chain_id AND block.number = events.block)
2795                ) as block_hash
2796            FROM (
2797            (SELECT
2798                'swap' as event_type,
2799                chain_id,
2800                pool_identifier,
2801                block,
2802                transaction_hash,
2803                transaction_index,
2804                log_index,
2805                COALESCE(
2806                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_swap_event.chain_id AND block.number = pool_swap_event.block),
2807                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_swap_event.chain_id AND pool_event_block.number = pool_swap_event.block)
2808                ) as block_timestamp,
2809                sender,
2810                recipient,
2811                NULL::TEXT as owner,
2812                sqrt_price_x96::TEXT,
2813                liquidity::TEXT as swap_liquidity,
2814                tick as swap_tick,
2815                amount0::TEXT as swap_amount0,
2816                amount1::TEXT as swap_amount1,
2817                NULL::TEXT as position_liquidity,
2818                NULL::TEXT as amount0,
2819                NULL::TEXT as amount1,
2820                NULL::INT as tick_lower,
2821                NULL::INT as tick_upper,
2822                NULL::TEXT as liquidity_event_type,
2823                NULL::TEXT as flash_amount0,
2824                NULL::TEXT as flash_amount1,
2825                NULL::TEXT as flash_paid0,
2826                NULL::TEXT as flash_paid1,
2827                NULL::INTEGER as fee_protocol0_new,
2828                NULL::INTEGER as fee_protocol1_new
2829            FROM pool_swap_event
2830            WHERE chain_id = $1 AND pool_identifier = $2
2831            AND ($3::BIGINT IS NULL OR block <= $3))
2832            UNION ALL
2833            (SELECT
2834                'liquidity' as event_type,
2835                chain_id,
2836                pool_identifier,
2837                block,
2838                transaction_hash,
2839                transaction_index,
2840                log_index,
2841                COALESCE(
2842                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_liquidity_event.chain_id AND block.number = pool_liquidity_event.block),
2843                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_liquidity_event.chain_id AND pool_event_block.number = pool_liquidity_event.block)
2844                ) as block_timestamp,
2845                sender,
2846                NULL::TEXT as recipient,
2847                owner,
2848                NULL::text as sqrt_price_x96,
2849                NULL::TEXT as swap_liquidity,
2850                NULL::INT as swap_tick,
2851                amount0::TEXT as swap_amount0,
2852                amount1::TEXT as swap_amount1,
2853                position_liquidity::TEXT,
2854                amount0::TEXT,
2855                amount1::TEXT,
2856                tick_lower::INT,
2857                tick_upper::INT,
2858                event_type as liquidity_event_type,
2859                NULL::TEXT as flash_amount0,
2860                NULL::TEXT as flash_amount1,
2861                NULL::TEXT as flash_paid0,
2862                NULL::TEXT as flash_paid1,
2863                NULL::INTEGER as fee_protocol0_new,
2864                NULL::INTEGER as fee_protocol1_new
2865            FROM pool_liquidity_event
2866            WHERE chain_id = $1 AND pool_identifier = $2
2867            AND ($3::BIGINT IS NULL OR block <= $3))
2868            UNION ALL
2869            (SELECT
2870                'collect' as event_type,
2871                chain_id,
2872                pool_identifier,
2873                block,
2874                transaction_hash,
2875                transaction_index,
2876                log_index,
2877                COALESCE(
2878                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_collect_event.chain_id AND block.number = pool_collect_event.block),
2879                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_collect_event.chain_id AND pool_event_block.number = pool_collect_event.block)
2880                ) as block_timestamp,
2881                NULL::TEXT as sender,
2882                NULL::TEXT as recipient,
2883                owner,
2884                NULL::TEXT as sqrt_price_x96,
2885                NULL::TEXT as swap_liquidity,
2886                NULL::INT AS swap_tick,
2887                amount0::TEXT as swap_amount0,
2888                amount1::TEXT as swap_amount1,
2889                NULL::TEXT as position_liquidity,
2890                amount0::TEXT,
2891                amount1::TEXT,
2892                tick_lower::INT,
2893                tick_upper::INT,
2894                NULL::TEXT as liquidity_event_type,
2895                NULL::TEXT as flash_amount0,
2896                NULL::TEXT as flash_amount1,
2897                NULL::TEXT as flash_paid0,
2898                NULL::TEXT as flash_paid1,
2899                NULL::INTEGER as fee_protocol0_new,
2900                NULL::INTEGER as fee_protocol1_new
2901            FROM pool_collect_event
2902            WHERE chain_id = $1 AND pool_identifier = $2
2903            AND ($3::BIGINT IS NULL OR block <= $3))
2904            UNION ALL
2905            (SELECT
2906                'fee_protocol_update' as event_type,
2907                chain_id,
2908                pool_identifier,
2909                block,
2910                transaction_hash,
2911                transaction_index,
2912                log_index,
2913                COALESCE(
2914                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_fee_protocol_update_event.chain_id AND block.number = pool_fee_protocol_update_event.block),
2915                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_fee_protocol_update_event.chain_id AND pool_event_block.number = pool_fee_protocol_update_event.block)
2916                ) as block_timestamp,
2917                NULL::TEXT as sender,
2918                NULL::TEXT as recipient,
2919                NULL::TEXT as owner,
2920                NULL::TEXT as sqrt_price_x96,
2921                NULL::TEXT as swap_liquidity,
2922                NULL::INT AS swap_tick,
2923                NULL::TEXT as swap_amount0,
2924                NULL::TEXT as swap_amount1,
2925                NULL::TEXT as position_liquidity,
2926                NULL::TEXT as amount0,
2927                NULL::TEXT as amount1,
2928                NULL::INT as tick_lower,
2929                NULL::INT as tick_upper,
2930                NULL::TEXT as liquidity_event_type,
2931                NULL::TEXT as flash_amount0,
2932                NULL::TEXT as flash_amount1,
2933                NULL::TEXT as flash_paid0,
2934                NULL::TEXT as flash_paid1,
2935                fee_protocol0_new::INTEGER,
2936                fee_protocol1_new::INTEGER
2937            FROM pool_fee_protocol_update_event
2938            WHERE chain_id = $1 AND pool_identifier = $2
2939            AND ($3::BIGINT IS NULL OR block <= $3))
2940            UNION ALL
2941            (SELECT
2942                'fee_protocol_collect' as event_type,
2943                chain_id,
2944                pool_identifier,
2945                block,
2946                transaction_hash,
2947                transaction_index,
2948                log_index,
2949                COALESCE(
2950                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_fee_protocol_collect_event.chain_id AND block.number = pool_fee_protocol_collect_event.block),
2951                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_fee_protocol_collect_event.chain_id AND pool_event_block.number = pool_fee_protocol_collect_event.block)
2952                ) as block_timestamp,
2953                sender,
2954                recipient,
2955                NULL::TEXT as owner,
2956                NULL::TEXT as sqrt_price_x96,
2957                NULL::TEXT as swap_liquidity,
2958                NULL::INT AS swap_tick,
2959                NULL::TEXT as swap_amount0,
2960                NULL::TEXT as swap_amount1,
2961                NULL::TEXT as position_liquidity,
2962                amount0::TEXT,
2963                amount1::TEXT,
2964                NULL::INT as tick_lower,
2965                NULL::INT as tick_upper,
2966                NULL::TEXT as liquidity_event_type,
2967                NULL::TEXT as flash_amount0,
2968                NULL::TEXT as flash_amount1,
2969                NULL::TEXT as flash_paid0,
2970                NULL::TEXT as flash_paid1,
2971                NULL::INTEGER as fee_protocol0_new,
2972                NULL::INTEGER as fee_protocol1_new
2973            FROM pool_fee_protocol_collect_event
2974            WHERE chain_id = $1 AND pool_identifier = $2
2975            AND ($3::BIGINT IS NULL OR block <= $3))
2976            UNION ALL
2977            (SELECT
2978                'flash' as event_type,
2979                chain_id,
2980                pool_identifier,
2981                block,
2982                transaction_hash,
2983                transaction_index,
2984                log_index,
2985                COALESCE(
2986                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_flash_event.chain_id AND block.number = pool_flash_event.block),
2987                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_flash_event.chain_id AND pool_event_block.number = pool_flash_event.block)
2988                ) as block_timestamp,
2989                sender,
2990                recipient,
2991                NULL::TEXT as owner,
2992                NULL::TEXT as sqrt_price_x96,
2993                NULL::TEXT as swap_liquidity,
2994                NULL::INT AS swap_tick,
2995                NULL::TEXT as swap_amount0,
2996                NULL::TEXT as swap_amount1,
2997                NULL::TEXT as position_liquidity,
2998                NULL::TEXT as amount0,
2999                NULL::TEXT as amount1,
3000                NULL::INT as tick_lower,
3001                NULL::INT as tick_upper,
3002                NULL::TEXT as liquidity_event_type,
3003                amount0::TEXT as flash_amount0,
3004                amount1::TEXT as flash_amount1,
3005                paid0::TEXT as flash_paid0,
3006                paid1::TEXT as flash_paid1,
3007                NULL::INTEGER as fee_protocol0_new,
3008                NULL::INTEGER as fee_protocol1_new
3009            FROM pool_flash_event
3010            WHERE chain_id = $1 AND pool_identifier = $2
3011            AND ($3::BIGINT IS NULL OR block <= $3))
3012            ) AS events
3013            ORDER BY events.block, events.transaction_index, events.log_index";
3014
3015        const QUERY_FROM_POSITION: &str = "
3016            SELECT events.*,
3017                COALESCE(
3018                    (SELECT hash FROM pool_event_block WHERE pool_event_block.chain_id = events.chain_id AND pool_event_block.number = events.block),
3019                    (SELECT hash FROM block WHERE block.chain_id = events.chain_id AND block.number = events.block)
3020                ) as block_hash
3021            FROM (
3022            (SELECT
3023                'swap' as event_type,
3024                chain_id,
3025                pool_identifier,
3026                block,
3027                transaction_hash,
3028                transaction_index,
3029                log_index,
3030                COALESCE(
3031                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_swap_event.chain_id AND block.number = pool_swap_event.block),
3032                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_swap_event.chain_id AND pool_event_block.number = pool_swap_event.block)
3033                ) as block_timestamp,
3034                sender,
3035                recipient,
3036                NULL::TEXT as owner,
3037                sqrt_price_x96::TEXT,
3038                liquidity::TEXT as swap_liquidity,
3039                tick as swap_tick,
3040                amount0::TEXT as swap_amount0,
3041                amount1::TEXT as swap_amount1,
3042                NULL::TEXT as position_liquidity,
3043                NULL::TEXT as amount0,
3044                NULL::TEXT as amount1,
3045                NULL::INT as tick_lower,
3046                NULL::INT as tick_upper,
3047                NULL::TEXT as liquidity_event_type,
3048                NULL::TEXT as flash_amount0,
3049                NULL::TEXT as flash_amount1,
3050                NULL::TEXT as flash_paid0,
3051                NULL::TEXT as flash_paid1,
3052                NULL::INTEGER as fee_protocol0_new,
3053                NULL::INTEGER as fee_protocol1_new
3054            FROM pool_swap_event
3055            WHERE chain_id = $1 AND pool_identifier = $2
3056            AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
3057            AND ($6::BIGINT IS NULL OR block <= $6))
3058            UNION ALL
3059            (SELECT
3060                'liquidity' as event_type,
3061                chain_id,
3062                pool_identifier,
3063                block,
3064                transaction_hash,
3065                transaction_index,
3066                log_index,
3067                COALESCE(
3068                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_liquidity_event.chain_id AND block.number = pool_liquidity_event.block),
3069                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_liquidity_event.chain_id AND pool_event_block.number = pool_liquidity_event.block)
3070                ) as block_timestamp,
3071                sender,
3072                NULL::TEXT as recipient,
3073                owner,
3074                NULL::text as sqrt_price_x96,
3075                NULL::TEXT as swap_liquidity,
3076                NULL::INT as swap_tick,
3077                amount0::TEXT as swap_amount0,
3078                amount1::TEXT as swap_amount1,
3079                position_liquidity::TEXT,
3080                amount0::TEXT,
3081                amount1::TEXT,
3082                tick_lower::INT,
3083                tick_upper::INT,
3084                event_type as liquidity_event_type,
3085                NULL::TEXT as flash_amount0,
3086                NULL::TEXT as flash_amount1,
3087                NULL::TEXT as flash_paid0,
3088                NULL::TEXT as flash_paid1,
3089                NULL::INTEGER as fee_protocol0_new,
3090                NULL::INTEGER as fee_protocol1_new
3091            FROM pool_liquidity_event
3092            WHERE chain_id = $1 AND pool_identifier = $2
3093            AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
3094            AND ($6::BIGINT IS NULL OR block <= $6))
3095            UNION ALL
3096            (SELECT
3097                'collect' as event_type,
3098                chain_id,
3099                pool_identifier,
3100                block,
3101                transaction_hash,
3102                transaction_index,
3103                log_index,
3104                COALESCE(
3105                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_collect_event.chain_id AND block.number = pool_collect_event.block),
3106                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_collect_event.chain_id AND pool_event_block.number = pool_collect_event.block)
3107                ) as block_timestamp,
3108                NULL::TEXT as sender,
3109                NULL::TEXT as recipient,
3110                owner,
3111                NULL::TEXT as sqrt_price_x96,
3112                NULL::TEXT as swap_liquidity,
3113                NULL::INT AS swap_tick,
3114                amount0::TEXT as swap_amount0,
3115                amount1::TEXT as swap_amount1,
3116                NULL::TEXT as position_liquidity,
3117                amount0::TEXT,
3118                amount1::TEXT,
3119                tick_lower::INT,
3120                tick_upper::INT,
3121                NULL::TEXT as liquidity_event_type,
3122                NULL::TEXT as flash_amount0,
3123                NULL::TEXT as flash_amount1,
3124                NULL::TEXT as flash_paid0,
3125                NULL::TEXT as flash_paid1,
3126                NULL::INTEGER as fee_protocol0_new,
3127                NULL::INTEGER as fee_protocol1_new
3128            FROM pool_collect_event
3129            WHERE chain_id = $1 AND pool_identifier = $2
3130            AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
3131            AND ($6::BIGINT IS NULL OR block <= $6))
3132            UNION ALL
3133            (SELECT
3134                'fee_protocol_update' as event_type,
3135                chain_id,
3136                pool_identifier,
3137                block,
3138                transaction_hash,
3139                transaction_index,
3140                log_index,
3141                COALESCE(
3142                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_fee_protocol_update_event.chain_id AND block.number = pool_fee_protocol_update_event.block),
3143                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_fee_protocol_update_event.chain_id AND pool_event_block.number = pool_fee_protocol_update_event.block)
3144                ) as block_timestamp,
3145                NULL::TEXT as sender,
3146                NULL::TEXT as recipient,
3147                NULL::TEXT as owner,
3148                NULL::TEXT as sqrt_price_x96,
3149                NULL::TEXT as swap_liquidity,
3150                NULL::INT AS swap_tick,
3151                NULL::TEXT as swap_amount0,
3152                NULL::TEXT as swap_amount1,
3153                NULL::TEXT as position_liquidity,
3154                NULL::TEXT as amount0,
3155                NULL::TEXT as amount1,
3156                NULL::INT as tick_lower,
3157                NULL::INT as tick_upper,
3158                NULL::TEXT as liquidity_event_type,
3159                NULL::TEXT as flash_amount0,
3160                NULL::TEXT as flash_amount1,
3161                NULL::TEXT as flash_paid0,
3162                NULL::TEXT as flash_paid1,
3163                fee_protocol0_new::INTEGER,
3164                fee_protocol1_new::INTEGER
3165            FROM pool_fee_protocol_update_event
3166            WHERE chain_id = $1 AND pool_identifier = $2
3167            AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
3168            AND ($6::BIGINT IS NULL OR block <= $6))
3169            UNION ALL
3170            (SELECT
3171                'fee_protocol_collect' as event_type,
3172                chain_id,
3173                pool_identifier,
3174                block,
3175                transaction_hash,
3176                transaction_index,
3177                log_index,
3178                COALESCE(
3179                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_fee_protocol_collect_event.chain_id AND block.number = pool_fee_protocol_collect_event.block),
3180                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_fee_protocol_collect_event.chain_id AND pool_event_block.number = pool_fee_protocol_collect_event.block)
3181                ) as block_timestamp,
3182                sender,
3183                recipient,
3184                NULL::TEXT as owner,
3185                NULL::TEXT as sqrt_price_x96,
3186                NULL::TEXT as swap_liquidity,
3187                NULL::INT AS swap_tick,
3188                NULL::TEXT as swap_amount0,
3189                NULL::TEXT as swap_amount1,
3190                NULL::TEXT as position_liquidity,
3191                amount0::TEXT,
3192                amount1::TEXT,
3193                NULL::INT as tick_lower,
3194                NULL::INT as tick_upper,
3195                NULL::TEXT as liquidity_event_type,
3196                NULL::TEXT as flash_amount0,
3197                NULL::TEXT as flash_amount1,
3198                NULL::TEXT as flash_paid0,
3199                NULL::TEXT as flash_paid1,
3200                NULL::INTEGER as fee_protocol0_new,
3201                NULL::INTEGER as fee_protocol1_new
3202            FROM pool_fee_protocol_collect_event
3203            WHERE chain_id = $1 AND pool_identifier = $2
3204            AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
3205            AND ($6::BIGINT IS NULL OR block <= $6))
3206            UNION ALL
3207            (SELECT
3208                'flash' as event_type,
3209                chain_id,
3210                pool_identifier,
3211                block,
3212                transaction_hash,
3213                transaction_index,
3214                log_index,
3215                COALESCE(
3216                    (SELECT timestamp::TEXT FROM block WHERE block.chain_id = pool_flash_event.chain_id AND block.number = pool_flash_event.block),
3217                    (SELECT timestamp::TEXT FROM pool_event_block WHERE pool_event_block.chain_id = pool_flash_event.chain_id AND pool_event_block.number = pool_flash_event.block)
3218                ) as block_timestamp,
3219                sender,
3220                recipient,
3221                NULL::TEXT as owner,
3222                NULL::TEXT as sqrt_price_x96,
3223                NULL::TEXT as swap_liquidity,
3224                NULL::INT AS swap_tick,
3225                NULL::TEXT as swap_amount0,
3226                NULL::TEXT as swap_amount1,
3227                NULL::TEXT as position_liquidity,
3228                NULL::TEXT as amount0,
3229                NULL::TEXT as amount1,
3230                NULL::INT as tick_lower,
3231                NULL::INT as tick_upper,
3232                NULL::TEXT as liquidity_event_type,
3233                amount0::TEXT as flash_amount0,
3234                amount1::TEXT as flash_amount1,
3235                paid0::TEXT as flash_paid0,
3236                paid1::TEXT as flash_paid1,
3237                NULL::INTEGER as fee_protocol0_new,
3238                NULL::INTEGER as fee_protocol1_new
3239            FROM pool_flash_event
3240            WHERE chain_id = $1 AND pool_identifier = $2
3241            AND (block > $3 OR (block = $3 AND transaction_index > $4) OR (block = $3 AND transaction_index = $4 AND log_index > $5))
3242            AND ($6::BIGINT IS NULL OR block <= $6))
3243            ) AS events
3244            ORDER BY events.block, events.transaction_index, events.log_index";
3245
3246        // Build query with appropriate bindings
3247        let query = if let Some(pos) = from_position {
3248            sqlx::query(QUERY_FROM_POSITION)
3249                .bind(chain.chain_id as i32)
3250                .bind(pool_identifier.to_string())
3251                .bind(pos.number as i64)
3252                .bind(pos.transaction_index as i32)
3253                .bind(pos.log_index as i32)
3254                .bind(to_block.map(|block| block as i64))
3255                .fetch(&self.pool)
3256        } else {
3257            sqlx::query(QUERY_ALL)
3258                .bind(chain.chain_id as i32)
3259                .bind(pool_identifier.to_string())
3260                .bind(to_block.map(|block| block as i64))
3261                .fetch(&self.pool)
3262        };
3263
3264        // Transform rows to events
3265        let stream = query.map(move |row_result| match row_result {
3266            Ok(row) => transform_row_to_dex_pool_data(
3267                &row,
3268                Arc::clone(&chain),
3269                Arc::clone(&dex),
3270                instrument_id,
3271            )
3272            .map_err(|e| anyhow::anyhow!("Steam pool event transform error: {e}")),
3273            Err(e) => Err(anyhow::anyhow!("Stream pool events database error: {e}")),
3274        });
3275
3276        Box::pin(stream)
3277    }
3278
3279    /// Persists an execution transaction record to the `execution_transaction` table.
3280    ///
3281    /// Records are written before broadcast so a signed transaction is never forgotten;
3282    /// the unique `(chain_id, transaction_hash)` constraint makes an exact re-insertion
3283    /// idempotent. Signer nonce ownership and order IDs are unique before broadcast. Order
3284    /// submission records carry the client order ID; operator transactions (wrap, approve)
3285    /// store `NULL`.
3286    ///
3287    /// # Errors
3288    ///
3289    /// Returns an error if the database operation fails.
3290    #[expect(
3291        clippy::too_many_arguments,
3292        reason = "the parameters mirror the persisted execution transaction fields"
3293    )]
3294    pub async fn add_execution_transaction(
3295        &self,
3296        chain_id: u32,
3297        wallet_address: &str,
3298        nonce: u64,
3299        transaction_hash: &str,
3300        purpose: &str,
3301        status: &str,
3302        client_order_id: Option<&str>,
3303    ) -> anyhow::Result<()> {
3304        let result = sqlx::query(
3305            "
3306            INSERT INTO execution_transaction (
3307                chain_id,
3308                wallet_address,
3309                nonce,
3310                transaction_hash,
3311                purpose,
3312                status,
3313                client_order_id
3314            )
3315            VALUES ($1, $2, $3, $4, $5, $6, $7)
3316            ON CONFLICT (chain_id, transaction_hash)
3317            DO UPDATE SET transaction_hash = EXCLUDED.transaction_hash
3318            WHERE execution_transaction.wallet_address = EXCLUDED.wallet_address
3319              AND execution_transaction.nonce = EXCLUDED.nonce
3320              AND execution_transaction.purpose = EXCLUDED.purpose
3321              AND execution_transaction.status = EXCLUDED.status
3322              AND execution_transaction.client_order_id IS NOT DISTINCT FROM EXCLUDED.client_order_id
3323        ",
3324        )
3325        .bind(chain_id as i32)
3326        .bind(wallet_address)
3327        .bind(nonce as i64)
3328        .bind(transaction_hash)
3329        .bind(purpose)
3330        .bind(status)
3331        .bind(client_order_id)
3332        .execute(&self.pool)
3333        .await
3334        .map_err(|e| anyhow::anyhow!("Failed to insert into execution_transaction table: {e}"))?;
3335
3336        anyhow::ensure!(
3337            result.rows_affected() == 1,
3338            "Execution transaction {transaction_hash} conflicts with its persisted record"
3339        );
3340        Ok(())
3341    }
3342
3343    /// Installs execution schema version 2 without changing existing transaction rows.
3344    ///
3345    /// The migration locks the legacy transaction table, refuses unresolved version 1 rows,
3346    /// installs the versioned intent and hash-history tables, and fences older writers before
3347    /// releasing the lock. This prevents a mixed-version process from bypassing the new signer
3348    /// ownership constraints.
3349    ///
3350    /// # Errors
3351    ///
3352    /// Returns an error if the database operation fails.
3353    pub async fn ensure_execution_transaction_schema(&self) -> anyhow::Result<()> {
3354        let mut transaction = self
3355            .pool
3356            .begin()
3357            .await
3358            .map_err(|e| anyhow::anyhow!("Failed to start execution schema migration: {e}"))?;
3359
3360        sqlx::query("LOCK TABLE execution_transaction IN ACCESS EXCLUSIVE MODE")
3361            .execute(&mut *transaction)
3362            .await
3363            .map_err(|e| anyhow::anyhow!("Failed to lock legacy execution transactions: {e}"))?;
3364
3365        for statement in [
3366            "
3367            ALTER TABLE execution_transaction
3368            ADD COLUMN IF NOT EXISTS client_order_id TEXT
3369            ",
3370            "
3371            ALTER TABLE execution_transaction
3372            ADD COLUMN IF NOT EXISTS wallet_address TEXT
3373            ",
3374            "
3375            ALTER TABLE execution_transaction
3376            ALTER COLUMN wallet_address DROP NOT NULL
3377            ",
3378            "
3379            CREATE TABLE IF NOT EXISTS execution_schema_version (
3380                component TEXT PRIMARY KEY,
3381                version SMALLINT NOT NULL CHECK (version > 0)
3382            )
3383            ",
3384            "
3385            CREATE TABLE IF NOT EXISTS execution_intent (
3386                id BIGSERIAL PRIMARY KEY,
3387                schema_version SMALLINT NOT NULL,
3388                chain_id INTEGER NOT NULL REFERENCES chain(chain_id) ON DELETE RESTRICT,
3389                wallet_address TEXT NOT NULL,
3390                nonce BIGINT,
3391                purpose TEXT NOT NULL,
3392                status TEXT NOT NULL,
3393                client_order_id TEXT,
3394                trader_id TEXT,
3395                strategy_id TEXT,
3396                account_id TEXT,
3397                instrument_id TEXT,
3398                pool_address TEXT,
3399                transaction_to TEXT NOT NULL,
3400                transaction_input TEXT NOT NULL,
3401                transaction_value TEXT NOT NULL,
3402                amount_in TEXT,
3403                created_block BIGINT NOT NULL,
3404                acknowledgement_emitted BOOLEAN NOT NULL DEFAULT FALSE,
3405                fill_emitted BOOLEAN NOT NULL DEFAULT FALSE,
3406                terminal_emitted BOOLEAN NOT NULL DEFAULT FALSE,
3407                active BOOLEAN NOT NULL DEFAULT TRUE,
3408                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3409                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3410                CHECK (schema_version = 2),
3411                CHECK (nonce IS NULL OR nonce >= 0),
3412                CHECK (created_block >= 0),
3413                CHECK (status IN (
3414                    'prepared', 'signed', 'broadcast', 'included', 'finalized',
3415                    'reverted', 'replaced', 'dropped', 'reorged', 'recoverable'
3416                )),
3417                CONSTRAINT execution_intent_active_check CHECK (
3418                    NOT active
3419                    OR status NOT IN ('finalized', 'reverted', 'recoverable')
3420                    OR (
3421                        status IN ('finalized', 'reverted')
3422                        AND NOT (fill_emitted OR terminal_emitted)
3423                    )
3424                ),
3425                CHECK (NOT (fill_emitted AND terminal_emitted)),
3426                CHECK (
3427                    purpose <> 'swap'
3428                    OR (
3429                        client_order_id IS NOT NULL
3430                        AND trader_id IS NOT NULL
3431                        AND strategy_id IS NOT NULL
3432                        AND account_id IS NOT NULL
3433                        AND instrument_id IS NOT NULL
3434                        AND pool_address IS NOT NULL
3435                        AND amount_in IS NOT NULL
3436                    )
3437                ),
3438                UNIQUE (id, chain_id)
3439            )
3440            ",
3441            "
3442            CREATE UNIQUE INDEX IF NOT EXISTS execution_intent_active_signer_key
3443            ON execution_intent (chain_id, wallet_address)
3444            WHERE active
3445            ",
3446            "
3447            CREATE UNIQUE INDEX IF NOT EXISTS execution_intent_active_nonce_key
3448            ON execution_intent (chain_id, wallet_address, nonce)
3449            WHERE active AND nonce IS NOT NULL
3450            ",
3451            "
3452            CREATE UNIQUE INDEX IF NOT EXISTS execution_intent_client_order_key
3453            ON execution_intent (chain_id, wallet_address, client_order_id)
3454            WHERE client_order_id IS NOT NULL
3455            ",
3456            "
3457            ALTER TABLE execution_intent
3458            DROP CONSTRAINT IF EXISTS execution_intent_active_check
3459            ",
3460            "
3461            ALTER TABLE execution_intent
3462            ADD CONSTRAINT execution_intent_active_check CHECK (
3463                NOT active
3464                OR status NOT IN ('finalized', 'reverted', 'recoverable')
3465                OR (
3466                    status IN ('finalized', 'reverted')
3467                    AND NOT (fill_emitted OR terminal_emitted)
3468                )
3469            )
3470            ",
3471            "
3472            CREATE TABLE IF NOT EXISTS execution_transaction_hash (
3473                id BIGSERIAL PRIMARY KEY,
3474                intent_id BIGINT NOT NULL,
3475                chain_id INTEGER NOT NULL,
3476                transaction_hash TEXT NOT NULL,
3477                payload_expected BOOLEAN NOT NULL DEFAULT TRUE,
3478                raw_transaction BYTEA,
3479                sealed_transaction BYTEA,
3480                status TEXT NOT NULL,
3481                block_number BIGINT,
3482                block_hash TEXT,
3483                receipt_success BOOLEAN,
3484                gas_used BIGINT,
3485                effective_gas_price TEXT,
3486                current BOOLEAN NOT NULL DEFAULT TRUE,
3487                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3488                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3489                FOREIGN KEY (intent_id, chain_id)
3490                    REFERENCES execution_intent(id, chain_id) ON DELETE RESTRICT,
3491                CHECK (block_number IS NULL OR block_number >= 0),
3492                CHECK (gas_used IS NULL OR gas_used >= 0),
3493                CONSTRAINT execution_transaction_raw_size_check
3494                    CHECK (raw_transaction IS NULL OR octet_length(raw_transaction) <= 131072),
3495                CONSTRAINT execution_transaction_sealed_size_check
3496                    CHECK (sealed_transaction IS NULL OR octet_length(sealed_transaction) <= 131133),
3497                CHECK (status IN (
3498                    'signed', 'broadcast', 'included', 'finalized', 'reverted',
3499                    'replaced', 'dropped', 'reorged'
3500                )),
3501                UNIQUE (chain_id, transaction_hash),
3502                UNIQUE (intent_id, transaction_hash)
3503            )
3504            ",
3505            "
3506            ALTER TABLE execution_transaction_hash
3507            ADD COLUMN IF NOT EXISTS payload_expected BOOLEAN
3508            ",
3509            "
3510            UPDATE execution_transaction_hash
3511            SET payload_expected = raw_transaction IS NOT NULL
3512            WHERE payload_expected IS NULL
3513            ",
3514            "
3515            ALTER TABLE execution_transaction_hash
3516            ALTER COLUMN payload_expected SET DEFAULT TRUE
3517            ",
3518            "
3519            ALTER TABLE execution_transaction_hash
3520            ALTER COLUMN payload_expected SET NOT NULL
3521            ",
3522            "
3523            ALTER TABLE execution_transaction_hash
3524            ADD COLUMN IF NOT EXISTS sealed_transaction BYTEA
3525            ",
3526            "
3527            ALTER TABLE execution_transaction_hash
3528            DROP CONSTRAINT IF EXISTS execution_transaction_raw_size_check
3529            ",
3530            "
3531            ALTER TABLE execution_transaction_hash
3532            ADD CONSTRAINT execution_transaction_raw_size_check
3533            CHECK (raw_transaction IS NULL OR octet_length(raw_transaction) <= 131072)
3534            ",
3535            "
3536            ALTER TABLE execution_transaction_hash
3537            DROP CONSTRAINT IF EXISTS execution_transaction_sealed_size_check
3538            ",
3539            "
3540            ALTER TABLE execution_transaction_hash
3541            ADD CONSTRAINT execution_transaction_sealed_size_check
3542            CHECK (sealed_transaction IS NULL OR octet_length(sealed_transaction) <= 131133)
3543            ",
3544            "
3545            CREATE TABLE IF NOT EXISTS execution_payload_state (
3546                component TEXT PRIMARY KEY CHECK (component = 'signed_transactions'),
3547                deployment_id TEXT NOT NULL CHECK (deployment_id <> ''),
3548                protocol_version SMALLINT NOT NULL CHECK (protocol_version = 1),
3549                operation TEXT NOT NULL CHECK (operation IN ('migrate', 'ready', 'rewrap', 'rollback')),
3550                active_key_id BYTEA NOT NULL CHECK (octet_length(active_key_id) = 32),
3551                progress_id BIGINT NOT NULL DEFAULT 0 CHECK (progress_id >= 0),
3552                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
3553            )
3554            ",
3555            "
3556            CREATE TABLE IF NOT EXISTS execution_payload_key_state (
3557                key_id BYTEA PRIMARY KEY CHECK (octet_length(key_id) = 32),
3558                seals BIGINT NOT NULL DEFAULT 0 CHECK (seals >= 0 AND seals < 4294967296)
3559            )
3560            ",
3561            "
3562            CREATE UNIQUE INDEX IF NOT EXISTS execution_transaction_hash_current_key
3563            ON execution_transaction_hash (intent_id)
3564            WHERE current
3565            ",
3566            "
3567            CREATE TABLE IF NOT EXISTS execution_transaction_transition (
3568                id BIGSERIAL PRIMARY KEY,
3569                intent_id BIGINT NOT NULL REFERENCES execution_intent(id) ON DELETE RESTRICT,
3570                transaction_hash_id BIGINT REFERENCES execution_transaction_hash(id) ON DELETE RESTRICT,
3571                transition_key TEXT NOT NULL,
3572                from_status TEXT,
3573                to_status TEXT NOT NULL,
3574                block_number BIGINT,
3575                block_hash TEXT,
3576                observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3577                CHECK (block_number IS NULL OR block_number >= 0),
3578                CHECK (to_status IN (
3579                    'prepared', 'signed', 'broadcast', 'included', 'finalized',
3580                    'reverted', 'replaced', 'dropped', 'reorged', 'recoverable'
3581                )),
3582                UNIQUE (intent_id, transition_key)
3583            )
3584            ",
3585        ] {
3586            sqlx::query(statement)
3587                .execute(&mut *transaction)
3588                .await
3589                .map_err(|e| {
3590                    anyhow::anyhow!("Failed to migrate execution_transaction table: {e}")
3591                })?;
3592        }
3593
3594        let installed_version = sqlx::query_scalar::<_, i16>(
3595            "SELECT version FROM execution_schema_version WHERE component = 'evm_execution'",
3596        )
3597        .fetch_optional(&mut *transaction)
3598        .await
3599        .map_err(|e| anyhow::anyhow!("Failed to read execution schema version: {e}"))?;
3600        if let Some(installed_version) = installed_version
3601            && installed_version > EXECUTION_SCHEMA_VERSION
3602        {
3603            anyhow::bail!(
3604                "Execution schema version {} is newer than supported version {EXECUTION_SCHEMA_VERSION}",
3605                installed_version
3606            );
3607        }
3608
3609        let unresolved_legacy = sqlx::query_scalar::<_, i64>(
3610            "
3611            SELECT COUNT(*)
3612            FROM execution_transaction
3613            WHERE status IN ('pending', 'included', 'reverted')
3614            ",
3615        )
3616        .fetch_one(&mut *transaction)
3617        .await
3618        .map_err(|e| anyhow::anyhow!("Failed to inspect legacy execution transactions: {e}"))?;
3619        anyhow::ensure!(
3620            unresolved_legacy == 0,
3621            "Cannot safely migrate {unresolved_legacy} unresolved execution schema version 1 transaction(s); resolve them with the prior version before enabling version {EXECUTION_SCHEMA_VERSION}"
3622        );
3623
3624        for statement in [
3625            "
3626            CREATE OR REPLACE FUNCTION execution_transaction_v2_fence()
3627            RETURNS TRIGGER AS $$
3628            BEGIN
3629                RAISE EXCEPTION 'Legacy execution writer refused after schema version 2 activation';
3630            END;
3631            $$ LANGUAGE plpgsql
3632            ",
3633            "DROP TRIGGER IF EXISTS execution_transaction_v2_fence ON execution_transaction",
3634            "
3635            CREATE TRIGGER execution_transaction_v2_fence
3636            BEFORE INSERT OR UPDATE OR DELETE ON execution_transaction
3637            FOR EACH STATEMENT EXECUTE FUNCTION execution_transaction_v2_fence()
3638            ",
3639            "
3640            CREATE OR REPLACE FUNCTION execution_transition_append_only()
3641            RETURNS TRIGGER AS $$
3642            BEGIN
3643                RAISE EXCEPTION 'Execution transitions are append-only';
3644            END;
3645            $$ LANGUAGE plpgsql
3646            ",
3647            "DROP TRIGGER IF EXISTS execution_transition_append_only ON execution_transaction_transition",
3648            "
3649            CREATE TRIGGER execution_transition_append_only
3650            BEFORE UPDATE OR DELETE ON execution_transaction_transition
3651            FOR EACH STATEMENT EXECUTE FUNCTION execution_transition_append_only()
3652            ",
3653            "
3654            INSERT INTO execution_schema_version (component, version)
3655            VALUES ('evm_execution', 2)
3656            ON CONFLICT (component) DO UPDATE SET version = EXCLUDED.version
3657            WHERE execution_schema_version.version <= EXCLUDED.version
3658            ",
3659        ] {
3660            sqlx::query(statement)
3661                .execute(&mut *transaction)
3662                .await
3663                .map_err(|e| {
3664                    anyhow::anyhow!("Failed to activate execution schema version 2: {e}")
3665                })?;
3666        }
3667
3668        transaction
3669            .commit()
3670            .await
3671            .map_err(|e| anyhow::anyhow!("Failed to commit execution schema migration: {e}"))?;
3672
3673        Ok(())
3674    }
3675
3676    /// Loads the durable finalized-header and nonce position when verification is active.
3677    pub(crate) async fn load_execution_verification_position(
3678        &self,
3679        chain_id: u32,
3680        wallet_address: &str,
3681        manifest_version: &str,
3682        manifest_digest: &str,
3683    ) -> anyhow::Result<Option<ExecutionVerificationPosition>> {
3684        let installed = sqlx::query_scalar::<_, i16>(
3685            "SELECT version FROM execution_schema_version \
3686             WHERE component = 'evm_execution_verification'",
3687        )
3688        .fetch_optional(&self.pool)
3689        .await
3690        .context("failed to inspect execution verification schema")?;
3691        let Some(installed) = installed else {
3692            return Ok(None);
3693        };
3694        anyhow::ensure!(
3695            installed <= VERIFICATION_SCHEMA_VERSION,
3696            "Unsupported execution verification schema version {installed}"
3697        );
3698        let chain_id =
3699            i32::try_from(chain_id).context("Verification chain ID exceeds PostgreSQL INTEGER")?;
3700        let current = sqlx::query_as::<_, (String, String, i64, i64)>(
3701            "
3702            SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
3703            FROM execution_verification_nonce
3704            WHERE chain_id = $1 AND wallet_address = $2
3705            ",
3706        )
3707        .bind(chain_id)
3708        .bind(wallet_address)
3709        .fetch_optional(&self.pool)
3710        .await
3711        .context("failed to load execution verification nonce position")?;
3712        let Some((stored_version, stored_digest, nonce, revision)) = current else {
3713            return Ok(None);
3714        };
3715        anyhow::ensure!(
3716            stored_version == manifest_version && stored_digest == manifest_digest,
3717            "Execution verification manifest identity changed"
3718        );
3719        let row = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
3720            "
3721            SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
3722            FROM execution_verified_finalized_header
3723            WHERE chain_id = $1 AND wallet_address = $2
3724            ORDER BY number DESC
3725            LIMIT 1
3726            ",
3727        )
3728        .bind(chain_id)
3729        .bind(wallet_address)
3730        .fetch_optional(&self.pool)
3731        .await
3732        .context("failed to load verified finalized header tip")?
3733        .ok_or_else(|| anyhow::anyhow!("Verified finalized header ledger is empty"))?;
3734        let (number, hash, parent_hash, timestamp, base_fee, digest) = row;
3735        anyhow::ensure!(
3736            digest == manifest_digest,
3737            "Finalized header manifest identity changed"
3738        );
3739        Ok(Some(ExecutionVerificationPosition {
3740            next_canonical_nonce: u64::try_from(nonce).context("Canonical nonce is negative")?,
3741            revision: u64::try_from(revision).context("Canonical nonce revision is negative")?,
3742            finalized_tip: ExecutionVerifiedHeader {
3743                number: u64::try_from(number).context("Finalized header number is negative")?,
3744                hash,
3745                parent_hash,
3746                timestamp: u64::try_from(timestamp)
3747                    .context("Finalized header timestamp is negative")?,
3748                base_fee_per_gas: base_fee
3749                    .map(|value| {
3750                        value
3751                            .parse::<u128>()
3752                            .map_err(|_| anyhow::anyhow!("Finalized header base fee is invalid"))
3753                    })
3754                    .transpose()?,
3755            },
3756        }))
3757    }
3758
3759    pub(crate) async fn load_execution_verified_header(
3760        &self,
3761        chain_id: u32,
3762        wallet_address: &str,
3763        number: u64,
3764        manifest_digest: &str,
3765    ) -> anyhow::Result<Option<ExecutionVerifiedHeader>> {
3766        let chain_id =
3767            i32::try_from(chain_id).context("Verification chain ID exceeds PostgreSQL INTEGER")?;
3768        let number =
3769            i64::try_from(number).context("Finalized header number exceeds PostgreSQL BIGINT")?;
3770        let row = sqlx::query_as::<_, (String, String, i64, Option<String>, String)>(
3771            "
3772            SELECT hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
3773            FROM execution_verified_finalized_header
3774            WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
3775            ",
3776        )
3777        .bind(chain_id)
3778        .bind(wallet_address)
3779        .bind(number)
3780        .fetch_optional(&self.pool)
3781        .await
3782        .context("failed to load verified finalized header")?;
3783        let Some((hash, parent_hash, timestamp, base_fee, digest)) = row else {
3784            return Ok(None);
3785        };
3786        anyhow::ensure!(
3787            digest == manifest_digest,
3788            "Finalized header manifest identity changed"
3789        );
3790        Ok(Some(ExecutionVerifiedHeader {
3791            number: u64::try_from(number).context("Finalized header number is negative")?,
3792            hash,
3793            parent_hash,
3794            timestamp: u64::try_from(timestamp)
3795                .context("Finalized header timestamp is negative")?,
3796            base_fee_per_gas: base_fee
3797                .map(|value| {
3798                    value
3799                        .parse::<u128>()
3800                        .map_err(|_| anyhow::anyhow!("Finalized header base fee is invalid"))
3801                })
3802                .transpose()?,
3803        }))
3804    }
3805
3806    /// Loads the retained execution state that a first verification bootstrap must classify.
3807    pub(crate) async fn load_execution_verification_migration_snapshot(
3808        &self,
3809        chain_id: u32,
3810        wallet_address: &str,
3811    ) -> anyhow::Result<ExecutionVerificationMigrationSnapshot> {
3812        let chain_id =
3813            i32::try_from(chain_id).context("Verification chain ID exceeds PostgreSQL INTEGER")?;
3814        let intents = sqlx::query_as::<_, ExecutionIntentRow>(
3815            "
3816            SELECT
3817                id, schema_version, chain_id, wallet_address, nonce, purpose, status,
3818                client_order_id, trader_id, strategy_id, account_id, instrument_id,
3819                pool_address, transaction_to, transaction_input, transaction_value,
3820                amount_in, created_block, acknowledgement_emitted, fill_emitted,
3821                terminal_emitted, active
3822            FROM execution_intent
3823            WHERE chain_id = $1 AND wallet_address = $2
3824            ORDER BY id
3825            ",
3826        )
3827        .bind(chain_id)
3828        .bind(wallet_address)
3829        .fetch_all(&self.pool)
3830        .await
3831        .context("failed to load retained execution intents for verification migration")?;
3832        let hashes = sqlx::query_as::<_, ExecutionTransactionHashRow>(
3833            "
3834            SELECT
3835                hash.id, hash.intent_id, hash.chain_id, hash.transaction_hash,
3836                hash.payload_expected, hash.raw_transaction, hash.sealed_transaction,
3837                hash.status, hash.block_number, hash.block_hash, hash.receipt_success,
3838                hash.gas_used, hash.effective_gas_price, hash.current
3839            FROM execution_transaction_hash AS hash
3840            JOIN execution_intent AS intent ON intent.id = hash.intent_id
3841            WHERE intent.chain_id = $1 AND intent.wallet_address = $2
3842            ORDER BY hash.id
3843            ",
3844        )
3845        .bind(chain_id)
3846        .bind(wallet_address)
3847        .fetch_all(&self.pool)
3848        .await
3849        .context("failed to load retained execution hashes for verification migration")?;
3850        Ok(ExecutionVerificationMigrationSnapshot { intents, hashes })
3851    }
3852
3853    /// Installs or validates the independent verification ledger for an execution signer.
3854    ///
3855    /// A database with retained execution intents but no verification ledger requires an archive-
3856    /// verified migration which classifies every retained intent. An empty database initializes
3857    /// its canonical nonce from a verified finalized-height transaction count and its header
3858    /// ledger from the trusted checkpoint.
3859    ///
3860    /// # Errors
3861    ///
3862    /// Returns an error if the schema, migration snapshot, historical reconstruction, or retained
3863    /// evidence is inconsistent, or persistence fails.
3864    pub(crate) async fn ensure_execution_verification_schema(
3865        &self,
3866        bootstrap: &ExecutionVerificationBootstrap<'_>,
3867    ) -> anyhow::Result<()> {
3868        let first_header = bootstrap
3869            .finalized_headers
3870            .first()
3871            .ok_or_else(|| anyhow::anyhow!("Verified finalized header ledger is empty"))?;
3872        anyhow::ensure!(
3873            bootstrap.finalized_headers.windows(2).all(|headers| {
3874                headers[1].number == headers[0].number.saturating_add(1)
3875                    && headers[1].parent_hash == headers[0].hash
3876            }),
3877            "Verified finalized headers are not one continuous parent-linked chain"
3878        );
3879        anyhow::ensure!(
3880            bootstrap.provider_ids.len() == 3
3881                && bootstrap.operator_ids.len() == 3
3882                && bootstrap.failure_domain_ids.len() >= 3
3883                && !bootstrap.decisions.is_empty(),
3884            "Connect verification evidence is incomplete"
3885        );
3886        let chain_id = i32::try_from(bootstrap.chain_id)
3887            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
3888        let checkpoint_number = i64::try_from(bootstrap.checkpoint_number)
3889            .context("Verification checkpoint exceeds PostgreSQL BIGINT")?;
3890        let checkpoint_timestamp = i64::try_from(bootstrap.checkpoint_timestamp)
3891            .context("Verification checkpoint timestamp exceeds PostgreSQL BIGINT")?;
3892        let next_canonical_nonce = i64::try_from(bootstrap.next_canonical_nonce)
3893            .context("Canonical nonce exceeds PostgreSQL BIGINT")?;
3894        let observed_canonical_nonce = i64::try_from(bootstrap.observed_canonical_nonce)
3895            .context("Observed canonical nonce exceeds PostgreSQL BIGINT")?;
3896        let base_fee = bootstrap
3897            .checkpoint_base_fee_per_gas
3898            .map(|value| value.to_string());
3899        let mut transaction = self.pool.begin().await.map_err(|e| {
3900            anyhow::anyhow!("Failed to start execution verification migration: {e}")
3901        })?;
3902
3903        for statement in [
3904            "
3905            CREATE TABLE IF NOT EXISTS execution_verification_nonce (
3906                chain_id INTEGER NOT NULL REFERENCES chain(chain_id) ON DELETE RESTRICT,
3907                wallet_address TEXT NOT NULL,
3908                manifest_version TEXT NOT NULL CHECK (manifest_version <> ''),
3909                manifest_digest TEXT NOT NULL CHECK (manifest_digest <> ''),
3910                next_canonical_nonce BIGINT NOT NULL CHECK (next_canonical_nonce >= 0),
3911                revision BIGINT NOT NULL DEFAULT 0 CHECK (revision >= 0),
3912                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3913                PRIMARY KEY (chain_id, wallet_address)
3914            )
3915            ",
3916            "
3917            CREATE TABLE IF NOT EXISTS execution_verified_finalized_header (
3918                chain_id INTEGER NOT NULL,
3919                wallet_address TEXT NOT NULL,
3920                number BIGINT NOT NULL CHECK (number >= 0),
3921                hash TEXT NOT NULL CHECK (hash <> ''),
3922                parent_hash TEXT NOT NULL CHECK (parent_hash <> ''),
3923                timestamp BIGINT NOT NULL CHECK (timestamp >= 0),
3924                base_fee_per_gas TEXT,
3925                manifest_digest TEXT NOT NULL CHECK (manifest_digest <> ''),
3926                observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3927                PRIMARY KEY (chain_id, wallet_address, number),
3928                UNIQUE (chain_id, wallet_address, hash),
3929                FOREIGN KEY (chain_id, wallet_address)
3930                    REFERENCES execution_verification_nonce(chain_id, wallet_address)
3931                    ON DELETE RESTRICT
3932            )
3933            ",
3934            "
3935            CREATE TABLE IF NOT EXISTS execution_verification_decision (
3936                id BIGSERIAL PRIMARY KEY,
3937                intent_id BIGINT REFERENCES execution_intent(id) ON DELETE RESTRICT,
3938                nonce BIGINT CHECK (nonce IS NULL OR nonce >= 0),
3939                decision_class TEXT NOT NULL CHECK (decision_class <> ''),
3940                read_class TEXT NOT NULL CHECK (read_class <> ''),
3941                height_start BIGINT CHECK (height_start IS NULL OR height_start >= 0),
3942                height_end BIGINT CHECK (height_end IS NULL OR height_end >= 0),
3943                manifest_version TEXT NOT NULL CHECK (manifest_version <> ''),
3944                manifest_digest TEXT NOT NULL CHECK (manifest_digest <> ''),
3945                provider_ids TEXT[] NOT NULL,
3946                operator_ids TEXT[] NOT NULL,
3947                failure_domain_ids TEXT[] NOT NULL,
3948                response_class TEXT NOT NULL CHECK (response_class <> ''),
3949                normalized_value_digest TEXT,
3950                safe_value JSONB,
3951                nonce_revision BIGINT CHECK (nonce_revision IS NULL OR nonce_revision >= 0),
3952                outcome TEXT NOT NULL CHECK (outcome IN (
3953                    'verified', 'disagreement', 'unavailable', 'retryable', 'locally_invalid'
3954                )),
3955                transition_key TEXT,
3956                observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3957                CHECK (cardinality(provider_ids) = 3),
3958                CHECK (cardinality(operator_ids) = 3),
3959                CHECK (cardinality(failure_domain_ids) >= 3),
3960                CHECK (safe_value IS NULL OR pg_column_size(safe_value) <= 16384),
3961                UNIQUE (intent_id, transition_key)
3962            )
3963            ",
3964            "
3965            CREATE TABLE IF NOT EXISTS execution_replacement_scan (
3966                intent_id BIGINT PRIMARY KEY REFERENCES execution_intent(id) ON DELETE RESTRICT,
3967                chain_id INTEGER NOT NULL,
3968                wallet_address TEXT NOT NULL,
3969                nonce BIGINT NOT NULL CHECK (nonce >= 0),
3970                finalized_cursor_number BIGINT NOT NULL CHECK (finalized_cursor_number >= 0),
3971                finalized_cursor_hash TEXT NOT NULL CHECK (finalized_cursor_hash <> ''),
3972                manifest_digest TEXT NOT NULL CHECK (manifest_digest <> ''),
3973                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3974                UNIQUE (chain_id, wallet_address, nonce)
3975            )
3976            ",
3977        ] {
3978            sqlx::query(statement)
3979                .execute(&mut *transaction)
3980                .await
3981                .map_err(|e| anyhow::anyhow!("Failed to install verification schema: {e}"))?;
3982        }
3983
3984        sqlx::query(
3985            "LOCK TABLE execution_intent, execution_transaction_hash, \
3986             execution_verification_nonce, execution_verified_finalized_header, \
3987             execution_verification_decision, execution_replacement_scan \
3988             IN ACCESS EXCLUSIVE MODE",
3989        )
3990        .execute(&mut *transaction)
3991        .await
3992        .map_err(|e| anyhow::anyhow!("Failed to lock execution verification state: {e}"))?;
3993
3994        let installed_version = sqlx::query_scalar::<_, i16>(
3995            "SELECT version FROM execution_schema_version \
3996             WHERE component = 'evm_execution_verification'",
3997        )
3998        .fetch_optional(&mut *transaction)
3999        .await
4000        .map_err(|e| anyhow::anyhow!("Failed to read verification schema version: {e}"))?;
4001        if let Some(installed_version) = installed_version {
4002            anyhow::ensure!(
4003                installed_version <= VERIFICATION_SCHEMA_VERSION,
4004                "Execution verification schema version {installed_version} is newer than supported version {VERIFICATION_SCHEMA_VERSION}"
4005            );
4006        }
4007
4008        let current = sqlx::query_as::<_, (String, String, i64, i64)>(
4009            "
4010            SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
4011            FROM execution_verification_nonce
4012            WHERE chain_id = $1 AND wallet_address = $2
4013            FOR UPDATE
4014            ",
4015        )
4016        .bind(chain_id)
4017        .bind(bootstrap.wallet_address)
4018        .fetch_optional(&mut *transaction)
4019        .await
4020        .map_err(|e| anyhow::anyhow!("Failed to read canonical nonce ledger: {e}"))?;
4021        let initialized = current.is_some();
4022
4023        let revision = if let Some((manifest_version, manifest_digest, stored_nonce, revision)) =
4024            current
4025        {
4026            anyhow::ensure!(
4027                bootstrap.migration.is_none(),
4028                "Verification migration was supplied for an initialized signer"
4029            );
4030            anyhow::ensure!(
4031                manifest_version == bootstrap.manifest_version
4032                    && manifest_digest == bootstrap.manifest_digest,
4033                "Execution verification manifest identity changed"
4034            );
4035            anyhow::ensure!(
4036                stored_nonce == next_canonical_nonce,
4037                "Canonical nonce ledger changed during verification bootstrap"
4038            );
4039
4040            if observed_canonical_nonce != stored_nonce {
4041                let expected_observed_nonce = stored_nonce
4042                    .checked_add(1)
4043                    .ok_or_else(|| anyhow::anyhow!("Canonical nonce overflow"))?;
4044                anyhow::ensure!(
4045                    observed_canonical_nonce == expected_observed_nonce,
4046                    "Verified finalized transaction count is outside the owned recovery range"
4047                );
4048                let recovery = sqlx::query_as::<_, (Option<i64>, String, i64)>(
4049                    "
4050                    SELECT
4051                        intent.nonce,
4052                        intent.status,
4053                        COUNT(hash.id) FILTER (
4054                            WHERE hash.current
4055                              AND hash.payload_expected
4056                              AND ((hash.raw_transaction IS NOT NULL)::INTEGER
4057                                   + (hash.sealed_transaction IS NOT NULL)::INTEGER) = 1
4058                              AND hash.status IN (
4059                                  'broadcast', 'included', 'replaced', 'dropped', 'reorged'
4060                              )
4061                        )
4062                    FROM execution_intent AS intent
4063                    LEFT JOIN execution_transaction_hash AS hash
4064                        ON hash.intent_id = intent.id
4065                    WHERE intent.chain_id = $1
4066                      AND intent.wallet_address = $2
4067                      AND intent.active
4068                    GROUP BY intent.id, intent.nonce, intent.status
4069                    ",
4070                )
4071                .bind(chain_id)
4072                .bind(bootstrap.wallet_address)
4073                .fetch_optional(&mut *transaction)
4074                .await
4075                .map_err(|e| anyhow::anyhow!("Failed to validate nonce recovery ownership: {e}"))?;
4076                let Some((intent_nonce, intent_status, payload_count)) = recovery else {
4077                    anyhow::bail!(
4078                        "Verified finalized transaction count advanced without an active owned intent"
4079                    );
4080                };
4081                anyhow::ensure!(
4082                    intent_nonce == Some(stored_nonce)
4083                        && matches!(
4084                            intent_status.as_str(),
4085                            "broadcast" | "included" | "replaced" | "dropped" | "reorged"
4086                        )
4087                        && payload_count == 1,
4088                    "Verified finalized transaction count advanced without one recoverable retained payload at the durable nonce"
4089                );
4090            }
4091            revision
4092        } else {
4093            anyhow::ensure!(
4094                next_canonical_nonce == observed_canonical_nonce,
4095                "Initial canonical nonce conflicts with the verified finalized transaction count"
4096            );
4097            let locked_intents = sqlx::query_as::<_, ExecutionIntentRow>(
4098                "
4099                SELECT
4100                    id, schema_version, chain_id, wallet_address, nonce, purpose, status,
4101                    client_order_id, trader_id, strategy_id, account_id, instrument_id,
4102                    pool_address, transaction_to, transaction_input, transaction_value,
4103                    amount_in, created_block, acknowledgement_emitted, fill_emitted,
4104                    terminal_emitted, active
4105                FROM execution_intent
4106                WHERE chain_id = $1 AND wallet_address = $2
4107                ORDER BY id
4108                ",
4109            )
4110            .bind(chain_id)
4111            .bind(bootstrap.wallet_address)
4112            .fetch_all(&mut *transaction)
4113            .await
4114            .map_err(|e| anyhow::anyhow!("Failed to lock retained execution intents: {e}"))?;
4115            let locked_hashes = sqlx::query_as::<_, ExecutionTransactionHashRow>(
4116                "
4117                SELECT
4118                    hash.id, hash.intent_id, hash.chain_id, hash.transaction_hash,
4119                    hash.payload_expected, hash.raw_transaction, hash.sealed_transaction,
4120                    hash.status, hash.block_number, hash.block_hash, hash.receipt_success,
4121                    hash.gas_used, hash.effective_gas_price, hash.current
4122                FROM execution_transaction_hash AS hash
4123                JOIN execution_intent AS intent ON intent.id = hash.intent_id
4124                WHERE intent.chain_id = $1 AND intent.wallet_address = $2
4125                ORDER BY hash.id
4126                ",
4127            )
4128            .bind(chain_id)
4129            .bind(bootstrap.wallet_address)
4130            .fetch_all(&mut *transaction)
4131            .await
4132            .map_err(|e| anyhow::anyhow!("Failed to lock retained execution hashes: {e}"))?;
4133            if locked_intents.is_empty() {
4134                anyhow::ensure!(
4135                    bootstrap.migration.is_none(),
4136                    "Verification migration was supplied for empty execution history"
4137                );
4138            } else {
4139                let migration = bootstrap.migration.ok_or_else(|| {
4140                    anyhow::anyhow!(
4141                        "Retained execution history requires archive verification before migration"
4142                    )
4143                })?;
4144                anyhow::ensure!(
4145                    migration.snapshot.intents == locked_intents
4146                        && migration.snapshot.hashes == locked_hashes,
4147                    "Retained execution history changed during verification migration"
4148                );
4149                let intent_ids = locked_intents
4150                    .iter()
4151                    .map(|intent| intent.id)
4152                    .collect::<BTreeSet<_>>();
4153                let record_ids = migration
4154                    .records
4155                    .iter()
4156                    .map(|record| record.intent_id)
4157                    .collect::<BTreeSet<_>>();
4158                anyhow::ensure!(
4159                    intent_ids == record_ids && record_ids.len() == migration.records.len(),
4160                    "Verification migration does not classify every retained intent exactly once"
4161                );
4162            }
4163            sqlx::query(
4164                "
4165                INSERT INTO execution_verification_nonce (
4166                    chain_id, wallet_address, manifest_version, manifest_digest,
4167                    next_canonical_nonce
4168                )
4169                VALUES ($1, $2, $3, $4, $5)
4170                ",
4171            )
4172            .bind(chain_id)
4173            .bind(bootstrap.wallet_address)
4174            .bind(bootstrap.manifest_version)
4175            .bind(bootstrap.manifest_digest)
4176            .bind(next_canonical_nonce)
4177            .execute(&mut *transaction)
4178            .await
4179            .map_err(|e| anyhow::anyhow!("Failed to initialize canonical nonce ledger: {e}"))?;
4180
4181            if let Some(migration) = bootstrap.migration {
4182                for record in &migration.records {
4183                    anyhow::ensure!(
4184                        !record.decisions.is_empty(),
4185                        "Verification migration evidence is empty for intent {}",
4186                        record.intent_id
4187                    );
4188
4189                    if record.recover_prepared {
4190                        anyhow::ensure!(
4191                            record.terminal_status.is_none()
4192                                && record.transaction_hash.is_none()
4193                                && record.nonce.is_none(),
4194                            "Prepared recovery migration record is inconsistent"
4195                        );
4196                        let result = sqlx::query(
4197                            "
4198                            UPDATE execution_intent
4199                            SET status = 'recoverable', active = FALSE, updated_at = NOW()
4200                            WHERE id = $1 AND active AND status = 'prepared' AND nonce IS NULL
4201                              AND NOT EXISTS (
4202                                  SELECT 1 FROM execution_transaction_hash WHERE intent_id = $1
4203                              )
4204                            ",
4205                        )
4206                        .bind(record.intent_id)
4207                        .execute(&mut *transaction)
4208                        .await
4209                        .map_err(|e| {
4210                            anyhow::anyhow!("Failed to recover migrated prepared intent: {e}")
4211                        })?;
4212                        anyhow::ensure!(
4213                            result.rows_affected() == 1,
4214                            "Prepared intent {} changed during verification migration",
4215                            record.intent_id
4216                        );
4217                    }
4218
4219                    if let Some(status) = record.terminal_status {
4220                        anyhow::ensure!(
4221                            matches!(
4222                                status,
4223                                TransactionStatus::Finalized | TransactionStatus::Reverted
4224                            ),
4225                            "Migration terminal status is not finalized or reverted"
4226                        );
4227                        let transaction_hash =
4228                            record.transaction_hash.as_deref().ok_or_else(|| {
4229                                anyhow::anyhow!("Terminal migration record has no transaction hash")
4230                            })?;
4231                        let block_number = i64::try_from(record.block_number.ok_or_else(|| {
4232                            anyhow::anyhow!("Terminal migration record has no block number")
4233                        })?)
4234                        .context("Migration block exceeds PostgreSQL BIGINT")?;
4235                        let block_hash = record.block_hash.as_deref().ok_or_else(|| {
4236                            anyhow::anyhow!("Terminal migration record has no block hash")
4237                        })?;
4238                        let gas_used = i64::try_from(record.gas_used.ok_or_else(|| {
4239                            anyhow::anyhow!("Terminal migration record has no gas usage")
4240                        })?)
4241                        .context("Migration gas usage exceeds PostgreSQL BIGINT")?;
4242                        let receipt_success = record.receipt_success.ok_or_else(|| {
4243                            anyhow::anyhow!("Terminal migration record has no receipt status")
4244                        })?;
4245                        anyhow::ensure!(
4246                            receipt_success == (status == TransactionStatus::Finalized),
4247                            "Migration receipt status conflicts with terminal status"
4248                        );
4249                        let current_status = sqlx::query_scalar::<_, String>(
4250                            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
4251                        )
4252                        .bind(record.intent_id)
4253                        .fetch_one(&mut *transaction)
4254                        .await
4255                        .map_err(|e| anyhow::anyhow!("Failed to lock migrated intent: {e}"))?;
4256                        let hash_result = sqlx::query(
4257                            "
4258                            UPDATE execution_transaction_hash
4259                            SET status = $3, block_number = $4, block_hash = $5,
4260                                receipt_success = $6, gas_used = $7,
4261                                effective_gas_price = $8, updated_at = NOW()
4262                            WHERE intent_id = $1 AND transaction_hash = $2 AND current
4263                              AND payload_expected
4264                            ",
4265                        )
4266                        .bind(record.intent_id)
4267                        .bind(transaction_hash)
4268                        .bind(status.as_str())
4269                        .bind(block_number)
4270                        .bind(block_hash)
4271                        .bind(receipt_success)
4272                        .bind(gas_used)
4273                        .bind(record.effective_gas_price.as_deref())
4274                        .execute(&mut *transaction)
4275                        .await
4276                        .map_err(|e| anyhow::anyhow!("Failed to reconstruct migrated hash: {e}"))?;
4277                        anyhow::ensure!(
4278                            hash_result.rows_affected() == 1,
4279                            "Migrated terminal transaction hash was not found"
4280                        );
4281                        sqlx::query(
4282                            "UPDATE execution_intent SET status = $2, updated_at = NOW() WHERE id = $1",
4283                        )
4284                        .bind(record.intent_id)
4285                        .bind(status.as_str())
4286                        .execute(&mut *transaction)
4287                        .await
4288                        .map_err(|e| anyhow::anyhow!("Failed to reconstruct migrated intent: {e}"))?;
4289                        if current_status != status.as_str() {
4290                            sqlx::query(
4291                                "
4292                                INSERT INTO execution_transaction_transition (
4293                                    intent_id, transaction_hash_id, transition_key,
4294                                    from_status, to_status, block_number, block_hash
4295                                )
4296                                SELECT $1, id, $3, $4, $5, $6, $7
4297                                FROM execution_transaction_hash
4298                                WHERE intent_id = $1 AND transaction_hash = $2
4299                                ",
4300                            )
4301                            .bind(record.intent_id)
4302                            .bind(transaction_hash)
4303                            .bind(format!("migration:{}:{transaction_hash}", status.as_str()))
4304                            .bind(current_status)
4305                            .bind(status.as_str())
4306                            .bind(block_number)
4307                            .bind(block_hash)
4308                            .execute(&mut *transaction)
4309                            .await
4310                            .map_err(|e| {
4311                                anyhow::anyhow!(
4312                                    "Failed to record migrated terminal transition: {e}"
4313                                )
4314                            })?;
4315                        }
4316                    }
4317
4318                    let nonce = record
4319                        .nonce
4320                        .map(i64::try_from)
4321                        .transpose()
4322                        .context("Migration nonce exceeds PostgreSQL BIGINT")?;
4323
4324                    for (index, decision) in record.decisions.iter().enumerate() {
4325                        let height_start = decision
4326                            .height_start
4327                            .map(i64::try_from)
4328                            .transpose()
4329                            .context("Migration evidence height exceeds PostgreSQL BIGINT")?;
4330                        let height_end = decision
4331                            .height_end
4332                            .map(i64::try_from)
4333                            .transpose()
4334                            .context("Migration evidence height exceeds PostgreSQL BIGINT")?;
4335                        sqlx::query(
4336                            "
4337                            INSERT INTO execution_verification_decision (
4338                                intent_id, nonce, decision_class, read_class,
4339                                height_start, height_end, manifest_version, manifest_digest,
4340                                provider_ids, operator_ids, failure_domain_ids,
4341                                response_class, normalized_value_digest, nonce_revision,
4342                                outcome, transition_key
4343                            )
4344                            VALUES (
4345                                $1, $2, 'migration', $3, $4, $5, $6, $7, $8, $9, $10,
4346                                'all_valid', $11, 0, 'verified', $12
4347                            )
4348                            ",
4349                        )
4350                        .bind(record.intent_id)
4351                        .bind(nonce)
4352                        .bind(decision.read_class)
4353                        .bind(height_start)
4354                        .bind(height_end)
4355                        .bind(bootstrap.manifest_version)
4356                        .bind(bootstrap.manifest_digest)
4357                        .bind(bootstrap.provider_ids)
4358                        .bind(bootstrap.operator_ids)
4359                        .bind(bootstrap.failure_domain_ids)
4360                        .bind(&decision.normalized_value_digest)
4361                        .bind(format!("migration:{}:{index}", record.intent_id))
4362                        .execute(&mut *transaction)
4363                        .await
4364                        .map_err(|e| {
4365                            anyhow::anyhow!("Failed to persist migration evidence: {e}")
4366                        })?;
4367                    }
4368                }
4369            }
4370            0
4371        };
4372
4373        sqlx::query(
4374            "
4375            INSERT INTO execution_verified_finalized_header (
4376                chain_id, wallet_address, number, hash, parent_hash, timestamp,
4377                base_fee_per_gas, manifest_digest
4378            )
4379            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
4380            ON CONFLICT (chain_id, wallet_address, number) DO NOTHING
4381            ",
4382        )
4383        .bind(chain_id)
4384        .bind(bootstrap.wallet_address)
4385        .bind(checkpoint_number)
4386        .bind(bootstrap.checkpoint_hash)
4387        .bind(bootstrap.checkpoint_parent_hash)
4388        .bind(checkpoint_timestamp)
4389        .bind(base_fee)
4390        .bind(bootstrap.manifest_digest)
4391        .execute(&mut *transaction)
4392        .await
4393        .map_err(|e| anyhow::anyhow!("Failed to initialize finalized header ledger: {e}"))?;
4394        let stored_checkpoint = sqlx::query_as::<_, (String, String, i64, Option<String>)>(
4395            "
4396            SELECT hash, parent_hash, timestamp, base_fee_per_gas
4397            FROM execution_verified_finalized_header
4398            WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
4399            ",
4400        )
4401        .bind(chain_id)
4402        .bind(bootstrap.wallet_address)
4403        .bind(checkpoint_number)
4404        .fetch_one(&mut *transaction)
4405        .await
4406        .map_err(|e| anyhow::anyhow!("Failed to validate finalized checkpoint ledger: {e}"))?;
4407        anyhow::ensure!(
4408            stored_checkpoint
4409                == (
4410                    bootstrap.checkpoint_hash.to_string(),
4411                    bootstrap.checkpoint_parent_hash.to_string(),
4412                    checkpoint_timestamp,
4413                    bootstrap
4414                        .checkpoint_base_fee_per_gas
4415                        .map(|value| value.to_string()),
4416                ),
4417            "Finalized checkpoint ledger conflicts with the trusted chain anchor"
4418        );
4419
4420        if initialized {
4421            let stored_tip =
4422                sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
4423                    "
4424                SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
4425                FROM execution_verified_finalized_header
4426                WHERE chain_id = $1 AND wallet_address = $2
4427                ORDER BY number DESC
4428                LIMIT 1
4429                ",
4430                )
4431                .bind(chain_id)
4432                .bind(bootstrap.wallet_address)
4433                .fetch_one(&mut *transaction)
4434                .await
4435                .map_err(|e| anyhow::anyhow!("Failed to lock finalized header tip: {e}"))?;
4436            anyhow::ensure!(
4437                stored_tip
4438                    == (
4439                        i64::try_from(first_header.number)
4440                            .context("Verified finalized height exceeds PostgreSQL BIGINT")?,
4441                        first_header.hash.clone(),
4442                        first_header.parent_hash.clone(),
4443                        i64::try_from(first_header.timestamp)
4444                            .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?,
4445                        first_header.base_fee_per_gas.map(|value| value.to_string()),
4446                        bootstrap.manifest_digest.to_string(),
4447                    ),
4448                "Verified finalized header extension does not start at the durable tip"
4449            );
4450        } else {
4451            anyhow::ensure!(
4452                first_header.number == bootstrap.checkpoint_number
4453                    && first_header.hash == bootstrap.checkpoint_hash
4454                    && first_header.parent_hash == bootstrap.checkpoint_parent_hash
4455                    && first_header.timestamp == bootstrap.checkpoint_timestamp
4456                    && first_header.base_fee_per_gas == bootstrap.checkpoint_base_fee_per_gas,
4457                "Verified finalized headers do not start at the trusted checkpoint"
4458            );
4459        }
4460
4461        for header in bootstrap.finalized_headers.iter().skip(1) {
4462            let number = i64::try_from(header.number)
4463                .context("Verified finalized height exceeds PostgreSQL BIGINT")?;
4464            let timestamp = i64::try_from(header.timestamp)
4465                .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?;
4466            let base_fee = header.base_fee_per_gas.map(|value| value.to_string());
4467            sqlx::query(
4468                "
4469                INSERT INTO execution_verified_finalized_header (
4470                    chain_id, wallet_address, number, hash, parent_hash, timestamp,
4471                    base_fee_per_gas, manifest_digest
4472                )
4473                VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
4474                ON CONFLICT (chain_id, wallet_address, number) DO NOTHING
4475                ",
4476            )
4477            .bind(chain_id)
4478            .bind(bootstrap.wallet_address)
4479            .bind(number)
4480            .bind(&header.hash)
4481            .bind(&header.parent_hash)
4482            .bind(timestamp)
4483            .bind(&base_fee)
4484            .bind(bootstrap.manifest_digest)
4485            .execute(&mut *transaction)
4486            .await
4487            .map_err(|e| anyhow::anyhow!("Failed to extend finalized header ledger: {e}"))?;
4488            let stored = sqlx::query_as::<_, (String, String, i64, Option<String>, String)>(
4489                "
4490                SELECT hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
4491                FROM execution_verified_finalized_header
4492                WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
4493                ",
4494            )
4495            .bind(chain_id)
4496            .bind(bootstrap.wallet_address)
4497            .bind(number)
4498            .fetch_one(&mut *transaction)
4499            .await
4500            .map_err(|e| anyhow::anyhow!("Failed to validate finalized header ledger: {e}"))?;
4501            anyhow::ensure!(
4502                stored
4503                    == (
4504                        header.hash.clone(),
4505                        header.parent_hash.clone(),
4506                        timestamp,
4507                        base_fee,
4508                        bootstrap.manifest_digest.to_string(),
4509                    ),
4510                "Finalized header ledger conflicts at height {}",
4511                header.number
4512            );
4513        }
4514
4515        let finalized_height = bootstrap
4516            .finalized_headers
4517            .last()
4518            .expect("verified finalized headers are nonempty")
4519            .number;
4520
4521        for (index, decision) in bootstrap.decisions.iter().enumerate() {
4522            let height_start = decision
4523                .height_start
4524                .map(i64::try_from)
4525                .transpose()
4526                .context("Connect verification height exceeds PostgreSQL BIGINT")?;
4527            let height_end = decision
4528                .height_end
4529                .map(i64::try_from)
4530                .transpose()
4531                .context("Connect verification height exceeds PostgreSQL BIGINT")?;
4532            let transition_key = format!("connect:{finalized_height}:{revision}:{index}");
4533            sqlx::query(
4534                "
4535                INSERT INTO execution_verification_decision (
4536                    intent_id, nonce, decision_class, read_class, height_start, height_end,
4537                    manifest_version, manifest_digest, provider_ids, operator_ids,
4538                    failure_domain_ids, response_class, normalized_value_digest,
4539                    nonce_revision, outcome, transition_key
4540                )
4541                VALUES (
4542                    NULL, NULL, 'connect', $1, $2, $3, $4, $5, $6, $7, $8,
4543                    'all_valid', $9, $10, 'verified', $11
4544                )
4545                ",
4546            )
4547            .bind(decision.read_class)
4548            .bind(height_start)
4549            .bind(height_end)
4550            .bind(bootstrap.manifest_version)
4551            .bind(bootstrap.manifest_digest)
4552            .bind(bootstrap.provider_ids)
4553            .bind(bootstrap.operator_ids)
4554            .bind(bootstrap.failure_domain_ids)
4555            .bind(&decision.normalized_value_digest)
4556            .bind(revision)
4557            .bind(transition_key)
4558            .execute(&mut *transaction)
4559            .await
4560            .map_err(|e| anyhow::anyhow!("Failed to persist connect verification: {e}"))?;
4561        }
4562
4563        for statement in [
4564            "
4565            CREATE OR REPLACE FUNCTION execution_verification_append_only()
4566            RETURNS TRIGGER AS $$
4567            BEGIN
4568                RAISE EXCEPTION 'Execution verification evidence is append-only';
4569            END;
4570            $$ LANGUAGE plpgsql
4571            ",
4572            "DROP TRIGGER IF EXISTS execution_verification_decision_append_only \
4573             ON execution_verification_decision",
4574            "
4575            CREATE TRIGGER execution_verification_decision_append_only
4576            BEFORE UPDATE OR DELETE ON execution_verification_decision
4577            FOR EACH STATEMENT EXECUTE FUNCTION execution_verification_append_only()
4578            ",
4579            "DROP TRIGGER IF EXISTS execution_finalized_header_append_only \
4580             ON execution_verified_finalized_header",
4581            "
4582            CREATE TRIGGER execution_finalized_header_append_only
4583            BEFORE UPDATE OR DELETE ON execution_verified_finalized_header
4584            FOR EACH STATEMENT EXECUTE FUNCTION execution_verification_append_only()
4585            ",
4586        ] {
4587            sqlx::query(statement)
4588                .execute(&mut *transaction)
4589                .await
4590                .map_err(|e| anyhow::anyhow!("Failed to activate verification schema: {e}"))?;
4591        }
4592        sqlx::query(
4593            "
4594            INSERT INTO execution_schema_version (component, version)
4595            VALUES ('evm_execution_verification', $1)
4596            ON CONFLICT (component) DO UPDATE SET version = EXCLUDED.version
4597            WHERE execution_schema_version.version <= EXCLUDED.version
4598            ",
4599        )
4600        .bind(VERIFICATION_SCHEMA_VERSION)
4601        .execute(&mut *transaction)
4602        .await
4603        .map_err(|e| anyhow::anyhow!("Failed to activate verification schema: {e}"))?;
4604
4605        transaction
4606            .commit()
4607            .await
4608            .map_err(|e| anyhow::anyhow!("Failed to commit verification migration: {e}"))?;
4609        Ok(())
4610    }
4611
4612    /// Activates or resumes protected signed-transaction storage for this database.
4613    pub(crate) async fn ensure_execution_payload_storage(
4614        &self,
4615        keys: &PayloadKeySet,
4616    ) -> anyhow::Result<()> {
4617        let marker = self.execution_payload_marker().await?;
4618        match marker {
4619            None => self.activate_execution_payload_storage(keys).await?,
4620            Some(version) => anyhow::ensure!(
4621                version == EXECUTION_PAYLOAD_PROTOCOL_VERSION,
4622                "Execution payload protection version {version} is newer than supported version {EXECUTION_PAYLOAD_PROTOCOL_VERSION}"
4623            ),
4624        }
4625
4626        loop {
4627            let state = self.execution_payload_state().await?.ok_or_else(|| {
4628                anyhow::anyhow!("Execution payload marker exists without durable state")
4629            })?;
4630            validate_execution_payload_state(&state, keys)?;
4631            match state.operation.as_str() {
4632                "migrate" => {
4633                    if self
4634                        .migrate_execution_payload_batch(keys, EXECUTION_PAYLOAD_BATCH_SIZE)
4635                        .await?
4636                    {
4637                        break;
4638                    }
4639                }
4640                "ready" => break,
4641                operation => anyhow::bail!(
4642                    "Execution payload storage is in {operation} maintenance; complete or roll back that operation before connecting"
4643                ),
4644            }
4645        }
4646
4647        self.validate_execution_payload_ready(keys).await
4648    }
4649
4650    /// Requires ready protected storage and authenticates every persisted signed payload.
4651    pub(crate) async fn require_execution_payload_storage(
4652        &self,
4653        keys: &PayloadKeySet,
4654        policy: PayloadPolicy,
4655        batch_size: i64,
4656    ) -> anyhow::Result<ExecutionPayloadLease> {
4657        self.require_execution_payload_storage_ready(keys).await?;
4658        let (check, transaction) = self
4659            .inspect_execution_payload_storage(Some(keys), Some(policy), batch_size)
4660            .await?;
4661        anyhow::ensure!(
4662            check.protected,
4663            "Postgres execution requires protected payload storage"
4664        );
4665        Ok(ExecutionPayloadLease {
4666            _transaction: transaction,
4667        })
4668    }
4669
4670    /// Requires protected storage to be ready before execution schema initialization.
4671    pub(crate) async fn require_execution_payload_storage_ready(
4672        &self,
4673        keys: &PayloadKeySet,
4674    ) -> anyhow::Result<()> {
4675        anyhow::ensure!(
4676            self.execution_payload_marker().await?.is_some(),
4677            "Postgres execution requires protected payload storage"
4678        );
4679        self.validate_execution_payload_ready(keys).await
4680    }
4681
4682    /// Acquires a shared state lease for signing, payload access, acknowledgment, or broadcast.
4683    pub(crate) async fn acquire_execution_payload_lease(
4684        &self,
4685        keys: &PayloadKeySet,
4686    ) -> anyhow::Result<ExecutionPayloadLease> {
4687        let mut transaction = self
4688            .pool
4689            .begin()
4690            .await
4691            .context("failed to start execution payload action lease")?;
4692        let marker = sqlx::query_scalar::<_, i16>(
4693            "SELECT version FROM execution_schema_version WHERE component = $1",
4694        )
4695        .bind(EXECUTION_PAYLOAD_COMPONENT)
4696        .fetch_optional(&mut *transaction)
4697        .await
4698        .context("failed to read execution payload marker")?;
4699
4700        let version = marker.ok_or_else(|| {
4701            anyhow::anyhow!("Postgres execution requires protected payload storage")
4702        })?;
4703        anyhow::ensure!(
4704            version == EXECUTION_PAYLOAD_PROTOCOL_VERSION,
4705            "Unsupported execution payload protection version {version}"
4706        );
4707        let row = sqlx::query(
4708            "SELECT deployment_id, protocol_version, operation, active_key_id \
4709             FROM execution_payload_state WHERE component = 'signed_transactions' \
4710             FOR SHARE",
4711        )
4712        .fetch_optional(&mut *transaction)
4713        .await
4714        .context("failed to lock execution payload state")?
4715        .ok_or_else(|| anyhow::anyhow!("Execution payload marker exists without state"))?;
4716        let state = execution_payload_state_from_row(&row)?;
4717        validate_execution_payload_state(&state, keys)?;
4718        anyhow::ensure!(
4719            state.operation == "ready",
4720            "Execution payload storage is in {} maintenance",
4721            state.operation
4722        );
4723
4724        Ok(ExecutionPayloadLease {
4725            _transaction: transaction,
4726        })
4727    }
4728
4729    /// Reserves one nonce use under the active payload key.
4730    pub(crate) async fn reserve_execution_payload_seal(
4731        &self,
4732        keys: &PayloadKeySet,
4733    ) -> anyhow::Result<()> {
4734        let mut transaction = self
4735            .pool
4736            .begin()
4737            .await
4738            .context("failed to start payload seal reservation")?;
4739        let row = sqlx::query(
4740            "SELECT deployment_id, protocol_version, operation, active_key_id \
4741             FROM execution_payload_state WHERE component = 'signed_transactions' FOR SHARE",
4742        )
4743        .fetch_optional(&mut *transaction)
4744        .await
4745        .context("failed to lock execution payload state")?
4746        .ok_or_else(|| anyhow::anyhow!("Execution payload protection is not active"))?;
4747        let state = execution_payload_state_from_row(&row)?;
4748        validate_execution_payload_state(&state, keys)?;
4749        anyhow::ensure!(
4750            state.operation == "ready",
4751            "Execution payload storage is in {} maintenance",
4752            state.operation
4753        );
4754        reserve_execution_payload_seal(&mut transaction, keys.active_key_id()).await?;
4755        transaction
4756            .commit()
4757            .await
4758            .context("failed to commit payload seal reservation")?;
4759        Ok(())
4760    }
4761
4762    async fn execution_payload_marker(&self) -> anyhow::Result<Option<i16>> {
4763        sqlx::query_scalar::<_, i16>(
4764            "SELECT version FROM execution_schema_version WHERE component = $1",
4765        )
4766        .bind(EXECUTION_PAYLOAD_COMPONENT)
4767        .fetch_optional(&self.pool)
4768        .await
4769        .context("failed to read execution payload marker")
4770    }
4771
4772    async fn execution_payload_state(&self) -> anyhow::Result<Option<ExecutionPayloadState>> {
4773        let row = sqlx::query(
4774            "SELECT deployment_id, protocol_version, operation, active_key_id \
4775             FROM execution_payload_state WHERE component = 'signed_transactions'",
4776        )
4777        .fetch_optional(&self.pool)
4778        .await
4779        .context("failed to read execution payload state")?;
4780        row.as_ref()
4781            .map(execution_payload_state_from_row)
4782            .transpose()
4783    }
4784
4785    async fn activate_execution_payload_storage(&self, keys: &PayloadKeySet) -> anyhow::Result<()> {
4786        let mut transaction = self
4787            .pool
4788            .begin()
4789            .await
4790            .context("failed to start execution payload activation")?;
4791        lock_execution_payload_operation(&mut transaction).await?;
4792        sqlx::query("LOCK TABLE execution_transaction_hash IN ACCESS EXCLUSIVE MODE")
4793            .execute(&mut *transaction)
4794            .await
4795            .context("failed to fence execution payload writers")?;
4796        let marker = sqlx::query_scalar::<_, i16>(
4797            "SELECT version FROM execution_schema_version WHERE component = $1",
4798        )
4799        .bind(EXECUTION_PAYLOAD_COMPONENT)
4800        .fetch_optional(&mut *transaction)
4801        .await
4802        .context("failed to recheck execution payload marker")?;
4803        if marker.is_some() {
4804            transaction
4805                .rollback()
4806                .await
4807                .context("failed to release concurrent payload activation")?;
4808            return Ok(());
4809        }
4810        let sealed_rows = sqlx::query_scalar::<_, i64>(
4811            "SELECT COUNT(*) FROM execution_transaction_hash WHERE sealed_transaction IS NOT NULL",
4812        )
4813        .fetch_one(&mut *transaction)
4814        .await
4815        .context("failed to inspect pre-activation envelopes")?;
4816        anyhow::ensure!(
4817            sealed_rows == 0,
4818            "Unprotected execution payload storage contains {sealed_rows} sealed row(s)"
4819        );
4820
4821        for statement in [
4822            "
4823            CREATE OR REPLACE FUNCTION execution_transaction_payload_fence()
4824            RETURNS TRIGGER AS $$
4825            DECLARE payload_operation TEXT;
4826            BEGIN
4827                IF NEW.raw_transaction IS NOT NULL
4828                   AND (TG_OP = 'INSERT'
4829                        OR OLD.raw_transaction IS NULL
4830                        OR NEW.raw_transaction IS DISTINCT FROM OLD.raw_transaction) THEN
4831                    SELECT operation INTO payload_operation
4832                    FROM execution_payload_state
4833                    WHERE component = 'signed_transactions';
4834                    IF NOT (
4835                        TG_OP = 'UPDATE'
4836                        AND payload_operation = 'rollback'
4837                        AND OLD.payload_expected
4838                        AND OLD.raw_transaction IS NULL
4839                        AND NEW.raw_transaction IS NOT NULL
4840                        AND OLD.sealed_transaction IS NOT NULL
4841                        AND NEW.sealed_transaction = OLD.sealed_transaction
4842                    ) THEN
4843                        RAISE EXCEPTION 'Plaintext signed transaction writes are disabled';
4844                    END IF;
4845                END IF;
4846                RETURN NEW;
4847            END;
4848            $$ LANGUAGE plpgsql
4849            ",
4850            "DROP TRIGGER IF EXISTS execution_transaction_payload_fence ON execution_transaction_hash",
4851            "
4852            CREATE TRIGGER execution_transaction_payload_fence
4853            BEFORE INSERT OR UPDATE ON execution_transaction_hash
4854            FOR EACH ROW EXECUTE FUNCTION execution_transaction_payload_fence()
4855            ",
4856        ] {
4857            sqlx::query(statement)
4858                .execute(&mut *transaction)
4859                .await
4860                .context("failed to install execution payload write fence")?;
4861        }
4862        sqlx::query("INSERT INTO execution_schema_version (component, version) VALUES ($1, $2)")
4863            .bind(EXECUTION_PAYLOAD_COMPONENT)
4864            .bind(EXECUTION_PAYLOAD_PROTOCOL_VERSION)
4865            .execute(&mut *transaction)
4866            .await
4867            .context("failed to record execution payload marker")?;
4868        sqlx::query(
4869            "INSERT INTO execution_payload_state (component, deployment_id, protocol_version, operation, active_key_id) \
4870             VALUES ('signed_transactions', $1, $2, 'migrate', $3)",
4871        )
4872        .bind(keys.deployment_id())
4873        .bind(EXECUTION_PAYLOAD_PROTOCOL_VERSION)
4874        .bind(keys.active_key_id().as_slice())
4875        .execute(&mut *transaction)
4876        .await
4877        .context("failed to record execution payload migration state")?;
4878        sqlx::query(
4879            "INSERT INTO execution_payload_key_state (key_id, seals) VALUES ($1, 0) \
4880             ON CONFLICT (key_id) DO NOTHING",
4881        )
4882        .bind(keys.active_key_id().as_slice())
4883        .execute(&mut *transaction)
4884        .await
4885        .context("failed to initialize execution payload key state")?;
4886        transaction
4887            .commit()
4888            .await
4889            .context("failed to commit execution payload activation")?;
4890        Ok(())
4891    }
4892
4893    async fn migrate_execution_payload_batch(
4894        &self,
4895        keys: &PayloadKeySet,
4896        batch_size: i64,
4897    ) -> anyhow::Result<bool> {
4898        anyhow::ensure!(
4899            batch_size > 0,
4900            "Execution payload batch size must be positive"
4901        );
4902        let mut transaction = self
4903            .pool
4904            .begin()
4905            .await
4906            .context("failed to start execution payload migration batch")?;
4907        lock_execution_payload_operation(&mut transaction).await?;
4908        let state_row = sqlx::query(
4909            "SELECT deployment_id, protocol_version, operation, active_key_id \
4910             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
4911        )
4912        .fetch_optional(&mut *transaction)
4913        .await
4914        .context("failed to lock execution payload migration state")?
4915        .ok_or_else(|| anyhow::anyhow!("Execution payload migration state is missing"))?;
4916        let state = execution_payload_state_from_row(&state_row)?;
4917        validate_execution_payload_state(&state, keys)?;
4918        anyhow::ensure!(
4919            state.operation == "migrate",
4920            "Execution payload storage is {}, not migrating",
4921            state.operation
4922        );
4923
4924        let rows = sqlx::query_as::<_, ExecutionTransactionHashRow>(
4925            "
4926            SELECT
4927                id, intent_id, chain_id, transaction_hash, payload_expected,
4928                raw_transaction, sealed_transaction, status, block_number, block_hash,
4929                receipt_success, gas_used, effective_gas_price, current
4930            FROM execution_transaction_hash
4931            WHERE payload_expected AND raw_transaction IS NOT NULL
4932            ORDER BY id
4933            LIMIT $1
4934            FOR UPDATE
4935            ",
4936        )
4937        .bind(batch_size)
4938        .fetch_all(&mut *transaction)
4939        .await
4940        .context("failed to load legacy signed transaction batch")?;
4941
4942        if rows.is_empty() {
4943            self.complete_execution_payload_migration(&mut transaction, keys)
4944                .await?;
4945            transaction
4946                .commit()
4947                .await
4948                .context("failed to commit execution payload migration completion")?;
4949            return Ok(true);
4950        }
4951
4952        for hash in rows {
4953            anyhow::ensure!(
4954                hash.sealed_transaction.is_none(),
4955                "Execution transaction {} contains both plaintext and sealed payloads",
4956                hash.id
4957            );
4958            let raw_transaction = hash
4959                .raw_transaction
4960                .as_deref()
4961                .expect("migration query requires plaintext");
4962            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
4963            let context = authenticate_retained_payload(
4964                raw_transaction,
4965                &intent,
4966                &hash,
4967                keys.deployment_id(),
4968            )
4969            .with_context(|| format!("failed to authenticate execution payload {}", hash.id))?;
4970            reserve_execution_payload_seal(&mut transaction, keys.active_key_id()).await?;
4971            let envelope = keys.seal(raw_transaction, &context)?;
4972            let unsealed = keys.unseal(&envelope, &context)?;
4973            authenticate_retained_payload(&unsealed, &intent, &hash, keys.deployment_id())?;
4974            anyhow::ensure!(
4975                unsealed == raw_transaction,
4976                "Execution payload {} changed during seal round trip",
4977                hash.id
4978            );
4979            let result = sqlx::query(
4980                "UPDATE execution_transaction_hash \
4981                 SET sealed_transaction = $2, raw_transaction = NULL, updated_at = NOW() \
4982                 WHERE id = $1 AND raw_transaction = $3 AND sealed_transaction IS NULL",
4983            )
4984            .bind(hash.id)
4985            .bind(&envelope)
4986            .bind(raw_transaction)
4987            .execute(&mut *transaction)
4988            .await
4989            .context("failed to promote execution payload")?;
4990            anyhow::ensure!(
4991                result.rows_affected() == 1,
4992                "Execution payload {} changed during migration",
4993                hash.id
4994            );
4995            sqlx::query(
4996                "UPDATE execution_payload_state SET progress_id = $1, updated_at = NOW() \
4997                 WHERE component = 'signed_transactions'",
4998            )
4999            .bind(hash.id)
5000            .execute(&mut *transaction)
5001            .await
5002            .context("failed to record execution payload migration progress")?;
5003        }
5004
5005        transaction
5006            .commit()
5007            .await
5008            .context("failed to commit execution payload migration batch")?;
5009        Ok(false)
5010    }
5011
5012    async fn complete_execution_payload_migration(
5013        &self,
5014        transaction: &mut Transaction<'_, Postgres>,
5015        keys: &PayloadKeySet,
5016    ) -> anyhow::Result<()> {
5017        sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE ROW EXCLUSIVE MODE")
5018            .execute(&mut **transaction)
5019            .await
5020            .context("failed to lock execution payload migration completion")?;
5021        let invalid = sqlx::query_scalar::<_, i64>(
5022            "SELECT COUNT(*) FROM execution_transaction_hash \
5023             WHERE (payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NULL)) \
5024                OR (NOT payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NOT NULL))",
5025        )
5026        .fetch_one(&mut **transaction)
5027        .await
5028        .context("failed to verify migrated execution payload representations")?;
5029        anyhow::ensure!(
5030            invalid == 0,
5031            "Execution payload migration found {invalid} invalid representation(s)"
5032        );
5033        validate_execution_payload_key_inventory(transaction, keys).await?;
5034
5035        for statement in [
5036            "ALTER TABLE execution_transaction_hash \
5037             DROP CONSTRAINT IF EXISTS execution_transaction_payload_protected_check",
5038            "ALTER TABLE execution_transaction_hash \
5039             ADD CONSTRAINT execution_transaction_payload_protected_check CHECK ( \
5040                 (payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NOT NULL) \
5041                 OR (NOT payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NULL) \
5042             )",
5043        ] {
5044            sqlx::query(statement)
5045                .execute(&mut **transaction)
5046                .await
5047                .context("failed to install protected execution payload constraint")?;
5048        }
5049        sqlx::query(
5050            "UPDATE execution_payload_state SET operation = 'ready', updated_at = NOW() \
5051             WHERE component = 'signed_transactions' AND operation = 'migrate'",
5052        )
5053        .execute(&mut **transaction)
5054        .await
5055        .context("failed to mark execution payload storage ready")?;
5056        Ok(())
5057    }
5058
5059    async fn validate_execution_payload_ready(&self, keys: &PayloadKeySet) -> anyhow::Result<()> {
5060        let mut transaction = self
5061            .pool
5062            .begin()
5063            .await
5064            .context("failed to start execution payload validation")?;
5065        let state_row = sqlx::query(
5066            "SELECT deployment_id, protocol_version, operation, active_key_id \
5067             FROM execution_payload_state WHERE component = 'signed_transactions' FOR SHARE",
5068        )
5069        .fetch_optional(&mut *transaction)
5070        .await
5071        .context("failed to lock execution payload state")?
5072        .ok_or_else(|| anyhow::anyhow!("Execution payload state is missing"))?;
5073        let state = execution_payload_state_from_row(&state_row)?;
5074        validate_execution_payload_state(&state, keys)?;
5075        anyhow::ensure!(
5076            state.operation == "ready",
5077            "Execution payload storage is not ready"
5078        );
5079        let (payload_fence, payload_constraint) =
5080            sqlx::query_as::<_, (bool, bool)>(EXECUTION_PAYLOAD_INFRASTRUCTURE_QUERY)
5081                .fetch_one(&mut *transaction)
5082                .await
5083                .context("failed to inspect execution payload protection infrastructure")?;
5084        anyhow::ensure!(
5085            payload_fence && payload_constraint,
5086            "Execution payload storage is marked ready without its write fence or constraint"
5087        );
5088        validate_execution_payload_key_inventory(&mut transaction, keys).await?;
5089        transaction
5090            .commit()
5091            .await
5092            .context("failed to complete execution payload validation")?;
5093        Ok(())
5094    }
5095
5096    pub(crate) async fn check_execution_payload_storage(
5097        &self,
5098        keys: Option<&PayloadKeySet>,
5099        policy: Option<PayloadPolicy>,
5100        batch_size: i64,
5101    ) -> anyhow::Result<ExecutionPayloadCheck> {
5102        let (check, transaction) = self
5103            .inspect_execution_payload_storage(keys, policy, batch_size)
5104            .await?;
5105        transaction
5106            .commit()
5107            .await
5108            .context("failed to complete execution payload check")?;
5109        Ok(check)
5110    }
5111
5112    async fn inspect_execution_payload_storage(
5113        &self,
5114        keys: Option<&PayloadKeySet>,
5115        policy: Option<PayloadPolicy>,
5116        batch_size: i64,
5117    ) -> anyhow::Result<(ExecutionPayloadCheck, Transaction<'static, Postgres>)> {
5118        anyhow::ensure!(
5119            batch_size > 0,
5120            "Execution payload check batch size must be positive"
5121        );
5122        let mut transaction = self
5123            .pool
5124            .begin()
5125            .await
5126            .context("failed to start execution payload check")?;
5127        sqlx::query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
5128            .execute(&mut *transaction)
5129            .await
5130            .context("failed to stabilize execution payload snapshot")?;
5131        sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE MODE")
5132            .execute(&mut *transaction)
5133            .await
5134            .context("failed to stabilize execution payload check")?;
5135        let marker = sqlx::query_scalar::<_, i16>(
5136            "SELECT version FROM execution_schema_version WHERE component = $1",
5137        )
5138        .bind(EXECUTION_PAYLOAD_COMPONENT)
5139        .fetch_optional(&mut *transaction)
5140        .await
5141        .context("failed to read execution payload marker")?;
5142        let deployment_id = match (marker, keys) {
5143            (None, _) => {
5144                let state = sqlx::query_scalar::<_, bool>(
5145                    "SELECT EXISTS (SELECT 1 FROM execution_payload_state) \
5146                     OR EXISTS (SELECT 1 FROM execution_transaction_hash \
5147                                WHERE sealed_transaction IS NOT NULL)",
5148                )
5149                .fetch_one(&mut *transaction)
5150                .await
5151                .context("failed to check legacy payload state")?;
5152                anyhow::ensure!(!state, "Legacy payload storage contains protected state");
5153                None
5154            }
5155            (Some(version), Some(keys)) => {
5156                anyhow::ensure!(
5157                    version == EXECUTION_PAYLOAD_PROTOCOL_VERSION,
5158                    "Unsupported execution payload protection version {version}"
5159                );
5160                let state_row = sqlx::query(
5161                    "SELECT deployment_id, protocol_version, operation, active_key_id \
5162                     FROM execution_payload_state WHERE component = 'signed_transactions' FOR SHARE",
5163                )
5164                .fetch_optional(&mut *transaction)
5165                .await
5166                .context("failed to lock execution payload state")?
5167                .ok_or_else(|| anyhow::anyhow!("Execution payload state is missing"))?;
5168                let state = execution_payload_state_from_row(&state_row)?;
5169                validate_execution_payload_state(&state, keys)?;
5170                anyhow::ensure!(
5171                    state.operation == "ready",
5172                    "Execution payload storage is not ready"
5173                );
5174                Some(state.deployment_id)
5175            }
5176            (Some(_), None) => anyhow::bail!(
5177                "Execution payload protection is active, but no payload key is configured"
5178            ),
5179        };
5180
5181        let mut cursor = 0_i64;
5182        let mut plaintext_rows = 0_u64;
5183        let mut original_rows = 0_u64;
5184        let mut replacement_rows = 0_u64;
5185        let mut authenticated_rows = 0_u64;
5186        let mut key_ids = BTreeSet::new();
5187
5188        loop {
5189            let rows = sqlx::query_as::<_, ExecutionTransactionHashRow>(
5190                "
5191                SELECT
5192                    id, intent_id, chain_id, transaction_hash, payload_expected,
5193                    raw_transaction, sealed_transaction, status, block_number, block_hash,
5194                    receipt_success, gas_used, effective_gas_price, current
5195                FROM execution_transaction_hash
5196                WHERE id > $1
5197                ORDER BY id
5198                LIMIT $2
5199                ",
5200            )
5201            .bind(cursor)
5202            .bind(batch_size)
5203            .fetch_all(&mut *transaction)
5204            .await
5205            .context("failed to load execution payload check batch")?;
5206
5207            if rows.is_empty() {
5208                break;
5209            }
5210
5211            for hash in &rows {
5212                cursor = hash.id;
5213                plaintext_rows += u64::from(hash.raw_transaction.is_some());
5214                if !hash.payload_expected {
5215                    replacement_rows += 1;
5216                    anyhow::ensure!(
5217                        hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
5218                        "Replacement execution transaction {} retains signed bytes",
5219                        hash.id
5220                    );
5221                    continue;
5222                }
5223                original_rows += 1;
5224                let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
5225                let raw_transaction = if let (Some(keys), Some(deployment_id)) =
5226                    (keys, deployment_id.as_deref())
5227                {
5228                    anyhow::ensure!(
5229                        hash.raw_transaction.is_none(),
5230                        "Protected execution transaction {} contains plaintext",
5231                        hash.id
5232                    );
5233                    let envelope = hash.sealed_transaction.as_deref().ok_or_else(|| {
5234                        anyhow::anyhow!(
5235                            "Protected execution transaction {} has no envelope",
5236                            hash.id
5237                        )
5238                    })?;
5239                    let key_id = envelope_key_id(envelope)?;
5240                    anyhow::ensure!(
5241                        keys.contains_key(&key_id),
5242                        "Execution transaction {} requires an unavailable payload key",
5243                        hash.id
5244                    );
5245                    key_ids.insert(alloy::hex::encode(key_id));
5246                    let context = payload_context(&intent, hash, deployment_id)?;
5247                    keys.unseal(envelope, &context)?
5248                } else {
5249                    anyhow::ensure!(
5250                        hash.sealed_transaction.is_none(),
5251                        "Legacy execution transaction {} contains an envelope",
5252                        hash.id
5253                    );
5254                    hash.raw_transaction.clone().ok_or_else(|| {
5255                        anyhow::anyhow!("Legacy execution transaction {} has no plaintext", hash.id)
5256                    })?
5257                };
5258                authenticate_retained_payload(
5259                    &raw_transaction,
5260                    &intent,
5261                    hash,
5262                    deployment_id.as_deref().unwrap_or(""),
5263                )?;
5264
5265                if let Some(policy) = policy
5266                    && retained_payload_requires_policy(&intent, hash, policy)?
5267                {
5268                    authenticate_payload(
5269                        &raw_transaction,
5270                        &intent,
5271                        hash,
5272                        policy,
5273                        deployment_id.as_deref().unwrap_or(""),
5274                    )
5275                    .with_context(|| {
5276                        format!(
5277                            "execution intent {} transaction {} violates current execution policy",
5278                            intent.id, hash.transaction_hash
5279                        )
5280                    })?;
5281                }
5282                authenticated_rows += 1;
5283            }
5284        }
5285        let read_roles = sqlx::query_scalar::<_, String>(
5286            "SELECT DISTINCT role_name FROM ( \
5287                 SELECT tableowner AS role_name FROM pg_tables \
5288                 WHERE schemaname = current_schema() AND tablename = 'execution_transaction_hash' \
5289                 UNION \
5290                 SELECT grantee AS role_name FROM information_schema.role_table_grants \
5291                 WHERE table_schema = current_schema() \
5292                   AND table_name = 'execution_transaction_hash' \
5293                   AND privilege_type = 'SELECT' \
5294             ) AS roles ORDER BY role_name",
5295        )
5296        .fetch_all(&mut *transaction)
5297        .await
5298        .context("failed to inspect execution payload read roles")?;
5299        Ok((
5300            ExecutionPayloadCheck {
5301                protected: deployment_id.is_some(),
5302                deployment_id,
5303                plaintext_rows,
5304                original_rows,
5305                replacement_rows,
5306                authenticated_rows,
5307                key_ids: key_ids.into_iter().collect(),
5308                read_roles,
5309            },
5310            transaction,
5311        ))
5312    }
5313
5314    pub(crate) async fn rewrap_execution_payload_storage(
5315        &self,
5316        keys: &PayloadKeySet,
5317        batch_size: i64,
5318    ) -> anyhow::Result<()> {
5319        anyhow::ensure!(
5320            batch_size > 0,
5321            "Execution payload rewrap batch size must be positive"
5322        );
5323        self.begin_execution_payload_rewrap(keys).await?;
5324
5325        loop {
5326            if self
5327                .rewrap_execution_payload_batch(keys, batch_size)
5328                .await?
5329            {
5330                return Ok(());
5331            }
5332        }
5333    }
5334
5335    async fn begin_execution_payload_rewrap(&self, keys: &PayloadKeySet) -> anyhow::Result<()> {
5336        let mut transaction = self
5337            .pool
5338            .begin()
5339            .await
5340            .context("failed to start execution payload rewrap")?;
5341        lock_execution_payload_operation(&mut transaction).await?;
5342        let state_row = sqlx::query(
5343            "SELECT deployment_id, protocol_version, operation, active_key_id \
5344             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
5345        )
5346        .fetch_optional(&mut *transaction)
5347        .await
5348        .context("failed to lock execution payload state for rewrap")?
5349        .ok_or_else(|| anyhow::anyhow!("Execution payload protection is not active"))?;
5350        let state = execution_payload_state_from_row(&state_row)?;
5351        anyhow::ensure!(
5352            state.protocol_version == EXECUTION_PAYLOAD_PROTOCOL_VERSION
5353                && state.deployment_id == keys.deployment_id(),
5354            "Execution payload rewrap context does not match this database"
5355        );
5356
5357        match state.operation.as_str() {
5358            "ready" => {
5359                let current_id: [u8; 32] = state
5360                    .active_key_id
5361                    .as_slice()
5362                    .try_into()
5363                    .context("database active payload key ID is invalid")?;
5364                anyhow::ensure!(
5365                    keys.contains_key(&current_id),
5366                    "Current database payload key is not configured for rewrap"
5367                );
5368                validate_execution_payload_key_inventory(&mut transaction, keys).await?;
5369                if current_id == *keys.active_key_id() {
5370                    transaction
5371                        .commit()
5372                        .await
5373                        .context("failed to complete no-op execution payload rewrap")?;
5374                    return Ok(());
5375                }
5376                sqlx::query(
5377                    "UPDATE execution_payload_state \
5378                     SET operation = 'rewrap', active_key_id = $1, progress_id = 0, updated_at = NOW() \
5379                     WHERE component = 'signed_transactions'",
5380                )
5381                .bind(keys.active_key_id().as_slice())
5382                .execute(&mut *transaction)
5383                .await
5384                .context("failed to record execution payload rewrap target")?;
5385            }
5386            "rewrap" => validate_execution_payload_state(&state, keys)?,
5387            operation => anyhow::bail!(
5388                "Execution payload storage is in {operation} maintenance, not ready for rewrap"
5389            ),
5390        }
5391        sqlx::query(
5392            "INSERT INTO execution_payload_key_state (key_id, seals) VALUES ($1, 0) \
5393             ON CONFLICT (key_id) DO NOTHING",
5394        )
5395        .bind(keys.active_key_id().as_slice())
5396        .execute(&mut *transaction)
5397        .await
5398        .context("failed to initialize rewrap target key state")?;
5399        transaction
5400            .commit()
5401            .await
5402            .context("failed to commit execution payload rewrap state")?;
5403        Ok(())
5404    }
5405
5406    async fn rewrap_execution_payload_batch(
5407        &self,
5408        keys: &PayloadKeySet,
5409        batch_size: i64,
5410    ) -> anyhow::Result<bool> {
5411        let mut transaction = self
5412            .pool
5413            .begin()
5414            .await
5415            .context("failed to start execution payload rewrap batch")?;
5416        lock_execution_payload_operation(&mut transaction).await?;
5417        let state_row = sqlx::query(
5418            "SELECT deployment_id, protocol_version, operation, active_key_id \
5419             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
5420        )
5421        .fetch_optional(&mut *transaction)
5422        .await
5423        .context("failed to lock execution payload rewrap state")?
5424        .ok_or_else(|| anyhow::anyhow!("Execution payload rewrap state is missing"))?;
5425        let state = execution_payload_state_from_row(&state_row)?;
5426        validate_execution_payload_state(&state, keys)?;
5427        if state.operation == "ready" {
5428            transaction
5429                .commit()
5430                .await
5431                .context("failed to complete execution payload rewrap")?;
5432            return Ok(true);
5433        }
5434        anyhow::ensure!(
5435            state.operation == "rewrap",
5436            "Execution payload storage is not rewrapping"
5437        );
5438        let rows = sqlx::query_as::<_, ExecutionTransactionHashRow>(
5439            "
5440            SELECT
5441                id, intent_id, chain_id, transaction_hash, payload_expected,
5442                raw_transaction, sealed_transaction, status, block_number, block_hash,
5443                receipt_success, gas_used, effective_gas_price, current
5444            FROM execution_transaction_hash
5445            WHERE payload_expected
5446              AND substring(sealed_transaction FROM 2 FOR 32) <> $1
5447            ORDER BY id
5448            LIMIT $2
5449            FOR UPDATE
5450            ",
5451        )
5452        .bind(keys.active_key_id().as_slice())
5453        .bind(batch_size)
5454        .fetch_all(&mut *transaction)
5455        .await
5456        .context("failed to load execution payload rewrap batch")?;
5457
5458        if rows.is_empty() {
5459            sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE ROW EXCLUSIVE MODE")
5460                .execute(&mut *transaction)
5461                .await
5462                .context("failed to lock execution payload rewrap completion")?;
5463            let remaining = sqlx::query_scalar::<_, i64>(
5464                "SELECT COUNT(*) FROM execution_transaction_hash \
5465                 WHERE payload_expected \
5466                   AND (raw_transaction IS NOT NULL OR sealed_transaction IS NULL \
5467                        OR substring(sealed_transaction FROM 2 FOR 32) <> $1)",
5468            )
5469            .bind(keys.active_key_id().as_slice())
5470            .fetch_one(&mut *transaction)
5471            .await
5472            .context("failed to verify execution payload rewrap completion")?;
5473            anyhow::ensure!(
5474                remaining == 0,
5475                "Execution payload rewrap left {remaining} row(s)"
5476            );
5477            sqlx::query(
5478                "UPDATE execution_payload_state SET operation = 'ready', updated_at = NOW() \
5479                 WHERE component = 'signed_transactions' AND operation = 'rewrap'",
5480            )
5481            .execute(&mut *transaction)
5482            .await
5483            .context("failed to mark execution payload rewrap complete")?;
5484            transaction
5485                .commit()
5486                .await
5487                .context("failed to commit execution payload rewrap completion")?;
5488            return Ok(true);
5489        }
5490
5491        for hash in rows {
5492            let envelope = hash.sealed_transaction.as_deref().ok_or_else(|| {
5493                anyhow::anyhow!(
5494                    "Execution payload {} has no envelope during rewrap",
5495                    hash.id
5496                )
5497            })?;
5498            anyhow::ensure!(
5499                hash.raw_transaction.is_none(),
5500                "Execution payload {} contains plaintext during rewrap",
5501                hash.id
5502            );
5503            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
5504            let context = payload_context(&intent, &hash, keys.deployment_id())?;
5505            let raw_transaction = keys.unseal(envelope, &context)?;
5506            authenticate_retained_payload(&raw_transaction, &intent, &hash, keys.deployment_id())?;
5507            reserve_execution_payload_seal(&mut transaction, keys.active_key_id()).await?;
5508            let rewrapped = keys.seal(&raw_transaction, &context)?;
5509            let verified = keys.unseal(&rewrapped, &context)?;
5510            authenticate_retained_payload(&verified, &intent, &hash, keys.deployment_id())?;
5511            anyhow::ensure!(
5512                verified == raw_transaction,
5513                "Execution payload {} changed during rewrap",
5514                hash.id
5515            );
5516            let result = sqlx::query(
5517                "UPDATE execution_transaction_hash SET sealed_transaction = $2, updated_at = NOW() \
5518                 WHERE id = $1 AND sealed_transaction = $3 AND raw_transaction IS NULL",
5519            )
5520            .bind(hash.id)
5521            .bind(&rewrapped)
5522            .bind(envelope)
5523            .execute(&mut *transaction)
5524            .await
5525            .context("failed to persist rewrapped execution payload")?;
5526            anyhow::ensure!(
5527                result.rows_affected() == 1,
5528                "Execution payload {} changed during rewrap",
5529                hash.id
5530            );
5531            sqlx::query(
5532                "UPDATE execution_payload_state SET progress_id = $1, updated_at = NOW() \
5533                 WHERE component = 'signed_transactions'",
5534            )
5535            .bind(hash.id)
5536            .execute(&mut *transaction)
5537            .await
5538            .context("failed to record execution payload rewrap progress")?;
5539        }
5540        transaction
5541            .commit()
5542            .await
5543            .context("failed to commit execution payload rewrap batch")?;
5544        Ok(false)
5545    }
5546
5547    pub(crate) async fn rollback_execution_payload_storage(
5548        &self,
5549        keys: &PayloadKeySet,
5550        batch_size: i64,
5551    ) -> anyhow::Result<()> {
5552        anyhow::ensure!(
5553            batch_size > 0,
5554            "Execution payload rollback batch size must be positive"
5555        );
5556        self.begin_execution_payload_rollback(keys).await?;
5557
5558        loop {
5559            if self
5560                .rollback_execution_payload_batch(keys, batch_size)
5561                .await?
5562            {
5563                return Ok(());
5564            }
5565        }
5566    }
5567
5568    async fn begin_execution_payload_rollback(&self, keys: &PayloadKeySet) -> anyhow::Result<()> {
5569        let mut transaction = self
5570            .pool
5571            .begin()
5572            .await
5573            .context("failed to start execution payload rollback")?;
5574        lock_execution_payload_operation(&mut transaction).await?;
5575        let state_row = sqlx::query(
5576            "SELECT deployment_id, protocol_version, operation, active_key_id \
5577             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
5578        )
5579        .fetch_optional(&mut *transaction)
5580        .await
5581        .context("failed to lock execution payload state for rollback")?
5582        .ok_or_else(|| anyhow::anyhow!("Execution payload protection is not active"))?;
5583        let state = execution_payload_state_from_row(&state_row)?;
5584        validate_execution_payload_state(&state, keys)?;
5585        match state.operation.as_str() {
5586            "ready" => {
5587                sqlx::query(
5588                    "ALTER TABLE execution_transaction_hash \
5589                     DROP CONSTRAINT IF EXISTS execution_transaction_payload_protected_check",
5590                )
5591                .execute(&mut *transaction)
5592                .await
5593                .context("failed to open execution payload rollback representation")?;
5594                sqlx::query(
5595                    "UPDATE execution_payload_state \
5596                     SET operation = 'rollback', progress_id = 0, updated_at = NOW() \
5597                     WHERE component = 'signed_transactions'",
5598                )
5599                .execute(&mut *transaction)
5600                .await
5601                .context("failed to record execution payload rollback state")?;
5602            }
5603            "rollback" => {}
5604            operation => anyhow::bail!(
5605                "Execution payload storage is in {operation} maintenance, not ready for rollback"
5606            ),
5607        }
5608        transaction
5609            .commit()
5610            .await
5611            .context("failed to commit execution payload rollback state")?;
5612        Ok(())
5613    }
5614
5615    async fn rollback_execution_payload_batch(
5616        &self,
5617        keys: &PayloadKeySet,
5618        batch_size: i64,
5619    ) -> anyhow::Result<bool> {
5620        let mut transaction = self
5621            .pool
5622            .begin()
5623            .await
5624            .context("failed to start execution payload rollback batch")?;
5625        lock_execution_payload_operation(&mut transaction).await?;
5626        let state_row = sqlx::query(
5627            "SELECT deployment_id, protocol_version, operation, active_key_id \
5628             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
5629        )
5630        .fetch_optional(&mut *transaction)
5631        .await
5632        .context("failed to lock execution payload rollback state")?
5633        .ok_or_else(|| anyhow::anyhow!("Execution payload rollback state is missing"))?;
5634        let state = execution_payload_state_from_row(&state_row)?;
5635        validate_execution_payload_state(&state, keys)?;
5636        anyhow::ensure!(
5637            state.operation == "rollback",
5638            "Execution payload storage is not rolling back"
5639        );
5640        let rows = sqlx::query_as::<_, ExecutionTransactionHashRow>(
5641            "
5642            SELECT
5643                id, intent_id, chain_id, transaction_hash, payload_expected,
5644                raw_transaction, sealed_transaction, status, block_number, block_hash,
5645                receipt_success, gas_used, effective_gas_price, current
5646            FROM execution_transaction_hash
5647            WHERE payload_expected AND sealed_transaction IS NOT NULL
5648            ORDER BY id
5649            LIMIT $1
5650            FOR UPDATE
5651            ",
5652        )
5653        .bind(batch_size)
5654        .fetch_all(&mut *transaction)
5655        .await
5656        .context("failed to load execution payload rollback batch")?;
5657
5658        if rows.is_empty() {
5659            sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE ROW EXCLUSIVE MODE")
5660                .execute(&mut *transaction)
5661                .await
5662                .context("failed to lock execution payload rollback completion")?;
5663            let invalid = sqlx::query_scalar::<_, i64>(
5664                "SELECT COUNT(*) FROM execution_transaction_hash \
5665                 WHERE (payload_expected AND (raw_transaction IS NULL OR sealed_transaction IS NOT NULL)) \
5666                    OR (NOT payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NOT NULL))",
5667            )
5668            .fetch_one(&mut *transaction)
5669            .await
5670            .context("failed to verify execution payload rollback completion")?;
5671            anyhow::ensure!(
5672                invalid == 0,
5673                "Execution payload rollback left {invalid} invalid row(s)"
5674            );
5675
5676            for statement in [
5677                "DROP TRIGGER IF EXISTS execution_transaction_payload_fence ON execution_transaction_hash",
5678                "DELETE FROM execution_payload_state WHERE component = 'signed_transactions'",
5679                "DELETE FROM execution_schema_version WHERE component = 'evm_execution_payload'",
5680            ] {
5681                sqlx::query(statement)
5682                    .execute(&mut *transaction)
5683                    .await
5684                    .context("failed to complete execution payload rollback")?;
5685            }
5686            transaction
5687                .commit()
5688                .await
5689                .context("failed to commit execution payload rollback completion")?;
5690            return Ok(true);
5691        }
5692
5693        for hash in rows {
5694            anyhow::ensure!(
5695                hash.raw_transaction.is_none(),
5696                "Execution payload {} contains both representations during rollback",
5697                hash.id
5698            );
5699            let envelope = hash
5700                .sealed_transaction
5701                .as_deref()
5702                .expect("rollback query requires envelope");
5703            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
5704            let context = payload_context(&intent, &hash, keys.deployment_id())?;
5705            let raw_transaction = keys.unseal(envelope, &context)?;
5706            authenticate_retained_payload(&raw_transaction, &intent, &hash, keys.deployment_id())?;
5707            let result = sqlx::query(
5708                "UPDATE execution_transaction_hash SET raw_transaction = $2, updated_at = NOW() \
5709                 WHERE id = $1 AND raw_transaction IS NULL AND sealed_transaction = $3",
5710            )
5711            .bind(hash.id)
5712            .bind(&raw_transaction)
5713            .bind(envelope)
5714            .execute(&mut *transaction)
5715            .await
5716            .context("failed to recreate plaintext execution payload")?;
5717            anyhow::ensure!(
5718                result.rows_affected() == 1,
5719                "Execution payload {} changed during rollback",
5720                hash.id
5721            );
5722            authenticate_retained_payload(&raw_transaction, &intent, &hash, keys.deployment_id())?;
5723            let result = sqlx::query(
5724                "UPDATE execution_transaction_hash SET sealed_transaction = NULL, updated_at = NOW() \
5725                 WHERE id = $1 AND raw_transaction = $2 AND sealed_transaction = $3",
5726            )
5727            .bind(hash.id)
5728            .bind(&raw_transaction)
5729            .bind(envelope)
5730            .execute(&mut *transaction)
5731            .await
5732            .context("failed to clear rolled-back execution payload envelope")?;
5733            anyhow::ensure!(
5734                result.rows_affected() == 1,
5735                "Execution payload {} changed while clearing rollback envelope",
5736                hash.id
5737            );
5738            sqlx::query(
5739                "UPDATE execution_payload_state SET progress_id = $1, updated_at = NOW() \
5740                 WHERE component = 'signed_transactions'",
5741            )
5742            .bind(hash.id)
5743            .execute(&mut *transaction)
5744            .await
5745            .context("failed to record execution payload rollback progress")?;
5746        }
5747        transaction
5748            .commit()
5749            .await
5750            .context("failed to commit execution payload rollback batch")?;
5751        Ok(false)
5752    }
5753
5754    /// Reserves durable ownership of a signer slot and optional client order before signing.
5755    ///
5756    /// # Errors
5757    ///
5758    /// Returns an error if the signer or client order is already owned, or persistence fails. A
5759    /// commit-stage error does not prove the transaction rolled back. The error identifies the
5760    /// failure stage.
5761    pub async fn reserve_execution_intent(
5762        &self,
5763        intent: &ExecutionIntentInsert,
5764    ) -> anyhow::Result<ExecutionIntentRow> {
5765        let (transaction, row) = async {
5766            let chain_id = i32::try_from(intent.chain_id).with_context(|| {
5767                format!("Chain ID {} exceeds PostgreSQL INTEGER", intent.chain_id)
5768            })?;
5769            let created_block = i64::try_from(intent.created_block).with_context(|| {
5770                format!(
5771                    "Execution creation block {} exceeds PostgreSQL BIGINT",
5772                    intent.created_block
5773                )
5774            })?;
5775            let mut transaction = self
5776                .pool
5777                .begin()
5778                .await
5779                .context("failed to start execution intent reservation")?;
5780            Self::lock_execution_signer(&mut transaction, intent.chain_id, &intent.wallet_address)
5781                .await?;
5782            let row = sqlx::query_as::<_, ExecutionIntentRow>(
5783                "
5784                INSERT INTO execution_intent (
5785                    schema_version, chain_id, wallet_address, purpose, status,
5786                    client_order_id, trader_id, strategy_id, account_id, instrument_id,
5787                    pool_address, transaction_to, transaction_input, transaction_value,
5788                    amount_in, created_block
5789                )
5790                VALUES ($1, $2, $3, $4, 'prepared', $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
5791                RETURNING
5792                    id, schema_version, chain_id, wallet_address, nonce, purpose, status,
5793                    client_order_id, trader_id, strategy_id, account_id, instrument_id,
5794                    pool_address, transaction_to, transaction_input, transaction_value,
5795                    amount_in, created_block, acknowledgement_emitted, fill_emitted,
5796                    terminal_emitted, active
5797                ",
5798            )
5799            .bind(EXECUTION_SCHEMA_VERSION)
5800            .bind(chain_id)
5801            .bind(&intent.wallet_address)
5802            .bind(&intent.purpose)
5803            .bind(&intent.client_order_id)
5804            .bind(&intent.trader_id)
5805            .bind(&intent.strategy_id)
5806            .bind(&intent.account_id)
5807            .bind(&intent.instrument_id)
5808            .bind(&intent.pool_address)
5809            .bind(&intent.transaction_to)
5810            .bind(&intent.transaction_input)
5811            .bind(&intent.transaction_value)
5812            .bind(&intent.amount_in)
5813            .bind(created_block)
5814            .fetch_one(&mut *transaction)
5815            .await
5816            .context("failed to insert execution intent reservation")?;
5817
5818            sqlx::query(
5819                "
5820                INSERT INTO execution_transaction_transition (
5821                    intent_id, transition_key, to_status, block_number
5822                ) VALUES ($1, 'prepared', 'prepared', $2)
5823                ",
5824            )
5825            .bind(row.id)
5826            .bind(created_block)
5827            .execute(&mut *transaction)
5828            .await
5829            .context("failed to record prepared execution intent")?;
5830
5831            Ok::<_, anyhow::Error>((transaction, row))
5832        }
5833        .await
5834        .map_err(|source| {
5835            anyhow::Error::new(ExecutionIntentReservationError {
5836                stage: ExecutionIntentReservationStage::BeforeCommit,
5837                source,
5838            })
5839        })?;
5840        transaction.commit().await.map_err(|e| {
5841            anyhow::Error::new(ExecutionIntentReservationError {
5842                stage: ExecutionIntentReservationStage::Commit,
5843                source: anyhow::Error::new(e)
5844                    .context("failed to commit execution intent reservation"),
5845            })
5846        })?;
5847        Ok(row)
5848    }
5849
5850    /// Assigns the signer nonce to a prepared execution intent.
5851    ///
5852    /// Repeating the same assignment is idempotent. A different nonce or non-prepared state
5853    /// fails closed.
5854    ///
5855    /// # Errors
5856    ///
5857    /// Returns an error if the intent cannot own the nonce or persistence fails.
5858    pub async fn assign_execution_intent_nonce(
5859        &self,
5860        intent_id: i64,
5861        nonce: u64,
5862    ) -> anyhow::Result<()> {
5863        let nonce_db = i64::try_from(nonce)
5864            .with_context(|| format!("Execution nonce {nonce} exceeds PostgreSQL BIGINT"))?;
5865        let result = sqlx::query(
5866            "
5867            UPDATE execution_intent
5868            SET nonce = $2, updated_at = NOW()
5869            WHERE id = $1
5870              AND status = 'prepared'
5871              AND (nonce IS NULL OR nonce = $2)
5872            ",
5873        )
5874        .bind(intent_id)
5875        .bind(nonce_db)
5876        .execute(&self.pool)
5877        .await
5878        .map_err(|e| anyhow::anyhow!("Failed to assign nonce {nonce} to execution intent: {e}"))?;
5879        anyhow::ensure!(
5880            result.rows_affected() == 1,
5881            "Execution intent {intent_id} is not prepared for nonce {nonce}"
5882        );
5883        Ok(())
5884    }
5885
5886    /// Assigns the canonical nonce and its authorizing verification evidence atomically.
5887    ///
5888    /// # Errors
5889    ///
5890    /// Returns an error if the durable nonce ledger, intent ownership, manifest identity, or
5891    /// evidence is inconsistent, or if persistence fails.
5892    pub(crate) async fn assign_execution_intent_nonce_verified(
5893        &self,
5894        assignment: &ExecutionNonceAssignment<'_>,
5895    ) -> anyhow::Result<()> {
5896        anyhow::ensure!(
5897            !assignment.decisions.is_empty(),
5898            "Verified nonce assignment requires decision evidence"
5899        );
5900        anyhow::ensure!(
5901            assignment.provider_ids.len() == 3 && assignment.operator_ids.len() == 3,
5902            "Verified nonce assignment requires exactly three provider and operator IDs"
5903        );
5904        anyhow::ensure!(
5905            assignment.failure_domain_ids.len() >= 3,
5906            "Verified nonce assignment requires the configured failure domains"
5907        );
5908        let chain_id = i32::try_from(assignment.chain_id)
5909            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
5910        let nonce =
5911            i64::try_from(assignment.nonce).context("Execution nonce exceeds PostgreSQL BIGINT")?;
5912        let mut transaction = self
5913            .pool
5914            .begin()
5915            .await
5916            .map_err(|e| anyhow::anyhow!("Failed to start verified nonce assignment: {e}"))?;
5917        let (manifest_version, manifest_digest, next_nonce, revision) =
5918            sqlx::query_as::<_, (String, String, i64, i64)>(
5919                "
5920                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
5921                FROM execution_verification_nonce
5922                WHERE chain_id = $1 AND wallet_address = $2
5923                FOR UPDATE
5924                ",
5925            )
5926            .bind(chain_id)
5927            .bind(assignment.wallet_address)
5928            .fetch_optional(&mut *transaction)
5929            .await
5930            .map_err(|e| anyhow::anyhow!("Failed to lock canonical nonce ledger: {e}"))?
5931            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
5932        anyhow::ensure!(
5933            manifest_version == assignment.manifest_version
5934                && manifest_digest == assignment.manifest_digest,
5935            "Verified nonce assignment manifest identity changed"
5936        );
5937        anyhow::ensure!(
5938            next_nonce == nonce,
5939            "Execution nonce {} does not match canonical nonce {next_nonce}",
5940            assignment.nonce
5941        );
5942
5943        let (intent_chain_id, intent_wallet, intent_nonce, intent_status, intent_active) =
5944            sqlx::query_as::<_, (i32, String, Option<i64>, String, bool)>(
5945                "
5946            SELECT chain_id, wallet_address, nonce, status, active
5947            FROM execution_intent
5948            WHERE id = $1
5949            FOR UPDATE
5950            ",
5951            )
5952            .bind(assignment.intent_id)
5953            .fetch_optional(&mut *transaction)
5954            .await
5955            .map_err(|e| {
5956                anyhow::anyhow!("Failed to lock execution intent for nonce assignment: {e}")
5957            })?
5958            .ok_or_else(|| {
5959                anyhow::anyhow!("Execution intent {} was not found", assignment.intent_id)
5960            })?;
5961        anyhow::ensure!(
5962            intent_chain_id == chain_id
5963                && intent_wallet == assignment.wallet_address
5964                && intent_status == "prepared"
5965                && intent_active
5966                && intent_nonce.is_none_or(|assigned| assigned == nonce),
5967            "Execution intent {} cannot own canonical nonce {}",
5968            assignment.intent_id,
5969            assignment.nonce
5970        );
5971
5972        for (index, decision) in assignment.decisions.iter().enumerate() {
5973            let height_start = decision
5974                .height_start
5975                .map(i64::try_from)
5976                .transpose()
5977                .context("Verification height exceeds PostgreSQL BIGINT")?;
5978            let height_end = decision
5979                .height_end
5980                .map(i64::try_from)
5981                .transpose()
5982                .context("Verification height exceeds PostgreSQL BIGINT")?;
5983            let transition_key = format!(
5984                "pre_sign:{}:{}:{index}",
5985                assignment.intent_id, decision.read_class
5986            );
5987            sqlx::query(
5988                "
5989                INSERT INTO execution_verification_decision (
5990                    intent_id, nonce, decision_class, read_class, height_start, height_end,
5991                    manifest_version, manifest_digest, provider_ids, operator_ids,
5992                    failure_domain_ids, response_class, normalized_value_digest,
5993                    nonce_revision, outcome, transition_key
5994                )
5995                VALUES (
5996                    $1, $2, 'pre_sign', $3, $4, $5, $6, $7, $8, $9, $10,
5997                    'all_valid', $11, $12, 'verified', $13
5998                )
5999                ",
6000            )
6001            .bind(assignment.intent_id)
6002            .bind(nonce)
6003            .bind(decision.read_class)
6004            .bind(height_start)
6005            .bind(height_end)
6006            .bind(assignment.manifest_version)
6007            .bind(assignment.manifest_digest)
6008            .bind(assignment.provider_ids)
6009            .bind(assignment.operator_ids)
6010            .bind(assignment.failure_domain_ids)
6011            .bind(&decision.normalized_value_digest)
6012            .bind(revision)
6013            .bind(transition_key)
6014            .execute(&mut *transaction)
6015            .await
6016            .map_err(|e| anyhow::anyhow!("Failed to persist pre-sign verification: {e}"))?;
6017        }
6018
6019        let result = sqlx::query(
6020            "
6021            UPDATE execution_intent
6022            SET nonce = $2, updated_at = NOW()
6023            WHERE id = $1
6024              AND status = 'prepared'
6025              AND active
6026              AND (nonce IS NULL OR nonce = $2)
6027            ",
6028        )
6029        .bind(assignment.intent_id)
6030        .bind(nonce)
6031        .execute(&mut *transaction)
6032        .await
6033        .map_err(|e| anyhow::anyhow!("Failed to assign verified execution nonce: {e}"))?;
6034        anyhow::ensure!(
6035            result.rows_affected() == 1,
6036            "Execution intent {} is not prepared for canonical nonce {}",
6037            assignment.intent_id,
6038            assignment.nonce
6039        );
6040        transaction
6041            .commit()
6042            .await
6043            .map_err(|e| anyhow::anyhow!("Failed to commit verified nonce assignment: {e}"))?;
6044        Ok(())
6045    }
6046
6047    /// Appends one verified decision batch before an action on an existing active intent.
6048    pub(crate) async fn record_execution_verification_batch(
6049        &self,
6050        batch: &ExecutionVerificationBatch<'_>,
6051    ) -> anyhow::Result<()> {
6052        anyhow::ensure!(
6053            !batch.decision_class.trim().is_empty() && !batch.decisions.is_empty(),
6054            "Verified action requires a decision class and evidence"
6055        );
6056        anyhow::ensure!(
6057            batch.provider_ids.len() == 3
6058                && batch.operator_ids.len() == 3
6059                && batch.failure_domain_ids.len() >= 3,
6060            "Verified action requires the configured provider identities"
6061        );
6062        let chain_id = i32::try_from(batch.chain_id)
6063            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
6064        let nonce =
6065            i64::try_from(batch.nonce).context("Execution nonce exceeds PostgreSQL BIGINT")?;
6066        let mut transaction = self
6067            .pool
6068            .begin()
6069            .await
6070            .map_err(|e| anyhow::anyhow!("Failed to start verified action evidence: {e}"))?;
6071        let (manifest_version, manifest_digest, revision) =
6072            sqlx::query_as::<_, (String, String, i64)>(
6073                "
6074                SELECT manifest_version, manifest_digest, revision
6075                FROM execution_verification_nonce
6076                WHERE chain_id = $1 AND wallet_address = $2
6077                FOR SHARE
6078                ",
6079            )
6080            .bind(chain_id)
6081            .bind(batch.wallet_address)
6082            .fetch_optional(&mut *transaction)
6083            .await
6084            .map_err(|e| anyhow::anyhow!("Failed to read verified action nonce ledger: {e}"))?
6085            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
6086        anyhow::ensure!(
6087            manifest_version == batch.manifest_version && manifest_digest == batch.manifest_digest,
6088            "Verified action manifest identity changed"
6089        );
6090        let intent_nonce = sqlx::query_scalar::<_, Option<i64>>(
6091            "
6092            SELECT nonce
6093            FROM execution_intent
6094            WHERE id = $1 AND chain_id = $2 AND wallet_address = $3 AND active
6095            FOR UPDATE
6096            ",
6097        )
6098        .bind(batch.intent_id)
6099        .bind(chain_id)
6100        .bind(batch.wallet_address)
6101        .fetch_optional(&mut *transaction)
6102        .await
6103        .map_err(|e| anyhow::anyhow!("Failed to lock intent for verified action: {e}"))?
6104        .ok_or_else(|| anyhow::anyhow!("Active verified-action intent was not found"))?;
6105        anyhow::ensure!(
6106            intent_nonce == Some(nonce),
6107            "Verified action nonce does not match the active intent"
6108        );
6109        let attempt = sqlx::query_scalar::<_, i64>(
6110            "
6111            SELECT COUNT(*)
6112            FROM execution_verification_decision
6113            WHERE intent_id = $1 AND decision_class = $2
6114            ",
6115        )
6116        .bind(batch.intent_id)
6117        .bind(batch.decision_class)
6118        .fetch_one(&mut *transaction)
6119        .await
6120        .map_err(|e| anyhow::anyhow!("Failed to number verified action evidence: {e}"))?;
6121
6122        for (index, decision) in batch.decisions.iter().enumerate() {
6123            let height_start = decision
6124                .height_start
6125                .map(i64::try_from)
6126                .transpose()
6127                .context("Verification height exceeds PostgreSQL BIGINT")?;
6128            let height_end = decision
6129                .height_end
6130                .map(i64::try_from)
6131                .transpose()
6132                .context("Verification height exceeds PostgreSQL BIGINT")?;
6133            let transition_key = format!(
6134                "{}:{}:{attempt}:{}:{index}",
6135                batch.decision_class, batch.intent_id, decision.read_class
6136            );
6137            sqlx::query(
6138                "
6139                INSERT INTO execution_verification_decision (
6140                    intent_id, nonce, decision_class, read_class, height_start, height_end,
6141                    manifest_version, manifest_digest, provider_ids, operator_ids,
6142                    failure_domain_ids, response_class, normalized_value_digest,
6143                    nonce_revision, outcome, transition_key
6144                )
6145                VALUES (
6146                    $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
6147                    'all_valid', $12, $13, 'verified', $14
6148                )
6149                ",
6150            )
6151            .bind(batch.intent_id)
6152            .bind(nonce)
6153            .bind(batch.decision_class)
6154            .bind(decision.read_class)
6155            .bind(height_start)
6156            .bind(height_end)
6157            .bind(batch.manifest_version)
6158            .bind(batch.manifest_digest)
6159            .bind(batch.provider_ids)
6160            .bind(batch.operator_ids)
6161            .bind(batch.failure_domain_ids)
6162            .bind(&decision.normalized_value_digest)
6163            .bind(revision)
6164            .bind(transition_key)
6165            .execute(&mut *transaction)
6166            .await
6167            .map_err(|e| anyhow::anyhow!("Failed to persist verified action evidence: {e}"))?;
6168        }
6169        transaction
6170            .commit()
6171            .await
6172            .map_err(|e| anyhow::anyhow!("Failed to commit verified action evidence: {e}"))?;
6173        Ok(())
6174    }
6175
6176    pub(crate) async fn load_execution_replacement_cursor(
6177        &self,
6178        intent_id: i64,
6179        chain_id: u32,
6180        wallet_address: &str,
6181        nonce: u64,
6182        manifest_digest: &str,
6183    ) -> anyhow::Result<Option<ExecutionVerifiedHeader>> {
6184        let chain_id = i32::try_from(chain_id)
6185            .context("Replacement scan chain ID exceeds PostgreSQL INTEGER")?;
6186        let nonce =
6187            i64::try_from(nonce).context("Replacement scan nonce exceeds PostgreSQL BIGINT")?;
6188        let row = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
6189            "
6190            SELECT
6191                h.number, h.hash, h.parent_hash, h.timestamp, h.base_fee_per_gas,
6192                s.manifest_digest
6193            FROM execution_replacement_scan AS s
6194            JOIN execution_verified_finalized_header AS h
6195              ON h.chain_id = s.chain_id
6196             AND h.wallet_address = s.wallet_address
6197             AND h.number = s.finalized_cursor_number
6198             AND h.hash = s.finalized_cursor_hash
6199            WHERE s.intent_id = $1
6200              AND s.chain_id = $2
6201              AND s.wallet_address = $3
6202              AND s.nonce = $4
6203            ",
6204        )
6205        .bind(intent_id)
6206        .bind(chain_id)
6207        .bind(wallet_address)
6208        .bind(nonce)
6209        .fetch_optional(&self.pool)
6210        .await
6211        .context("failed to load replacement scan cursor")?;
6212        row.map(
6213            |(number, hash, parent_hash, timestamp, base_fee, stored_digest)| {
6214                anyhow::ensure!(
6215                    stored_digest == manifest_digest,
6216                    "Replacement scan manifest identity changed"
6217                );
6218                Ok(ExecutionVerifiedHeader {
6219                    number: u64::try_from(number)
6220                        .context("Replacement scan cursor number is negative")?,
6221                    hash,
6222                    parent_hash,
6223                    timestamp: u64::try_from(timestamp)
6224                        .context("Replacement scan cursor timestamp is negative")?,
6225                    base_fee_per_gas: base_fee
6226                        .map(|value| {
6227                            value.parse::<u128>().map_err(|_| {
6228                                anyhow::anyhow!("Replacement scan cursor base fee is invalid")
6229                            })
6230                        })
6231                        .transpose()?,
6232                })
6233            },
6234        )
6235        .transpose()
6236    }
6237
6238    pub(crate) async fn record_execution_replacement_scan(
6239        &self,
6240        scan: &ExecutionReplacementScan<'_>,
6241    ) -> anyhow::Result<()> {
6242        anyhow::ensure!(
6243            !scan.decisions.is_empty()
6244                && scan.provider_ids.len() == 3
6245                && scan.operator_ids.len() == 3
6246                && scan.failure_domain_ids.len() >= 3,
6247            "Replacement scan verification evidence is incomplete"
6248        );
6249        let chain_id = i32::try_from(scan.chain_id)
6250            .context("Replacement scan chain ID exceeds PostgreSQL INTEGER")?;
6251        let nonce = i64::try_from(scan.nonce)
6252            .context("Replacement scan nonce exceeds PostgreSQL BIGINT")?;
6253        let mut transaction = self
6254            .pool
6255            .begin()
6256            .await
6257            .context("failed to start verified replacement scan transition")?;
6258        let (stored_version, stored_digest, stored_nonce, revision) =
6259            sqlx::query_as::<_, (String, String, i64, i64)>(
6260                "
6261                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
6262                FROM execution_verification_nonce
6263                WHERE chain_id = $1 AND wallet_address = $2
6264                FOR SHARE
6265                ",
6266            )
6267            .bind(chain_id)
6268            .bind(scan.wallet_address)
6269            .fetch_optional(&mut *transaction)
6270            .await
6271            .context("failed to read replacement scan nonce ledger")?
6272            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
6273        anyhow::ensure!(
6274            stored_version == scan.manifest_version
6275                && stored_digest == scan.manifest_digest
6276                && stored_nonce == nonce,
6277            "Replacement scan conflicts with the canonical nonce or manifest ledger"
6278        );
6279        let current_status = sqlx::query_scalar::<_, String>(
6280            "
6281            SELECT status
6282            FROM execution_intent
6283            WHERE id = $1 AND chain_id = $2 AND wallet_address = $3
6284              AND nonce = $4 AND active
6285            FOR UPDATE
6286            ",
6287        )
6288        .bind(scan.intent_id)
6289        .bind(chain_id)
6290        .bind(scan.wallet_address)
6291        .bind(nonce)
6292        .fetch_optional(&mut *transaction)
6293        .await
6294        .context("failed to lock the replacement scan intent")?
6295        .ok_or_else(|| anyhow::anyhow!("Active replacement scan intent was not found"))?;
6296
6297        if let Some(cursor) = scan.finalized_cursor {
6298            let number = i64::try_from(cursor.number)
6299                .context("Replacement scan cursor exceeds PostgreSQL BIGINT")?;
6300            let durable_hash = sqlx::query_scalar::<_, String>(
6301                "
6302                SELECT hash
6303                FROM execution_verified_finalized_header
6304                WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
6305                ",
6306            )
6307            .bind(chain_id)
6308            .bind(scan.wallet_address)
6309            .bind(number)
6310            .fetch_optional(&mut *transaction)
6311            .await
6312            .context("failed to validate replacement cursor against finalized headers")?
6313            .ok_or_else(|| {
6314                anyhow::anyhow!(
6315                    "Replacement scan cursor {} is not durably finalized",
6316                    cursor.number
6317                )
6318            })?;
6319            anyhow::ensure!(
6320                durable_hash == cursor.hash,
6321                "Replacement scan cursor conflicts with the finalized header ledger"
6322            );
6323            let existing = sqlx::query_as::<_, (i64, String)>(
6324                "
6325                SELECT finalized_cursor_number, finalized_cursor_hash
6326                FROM execution_replacement_scan
6327                WHERE intent_id = $1
6328                FOR UPDATE
6329                ",
6330            )
6331            .bind(scan.intent_id)
6332            .fetch_optional(&mut *transaction)
6333            .await
6334            .context("failed to lock replacement scan progress")?;
6335
6336            if let Some((stored_number, stored_hash)) = existing {
6337                anyhow::ensure!(
6338                    number > stored_number
6339                        || (number == stored_number && cursor.hash == stored_hash),
6340                    "Replacement scan cursor regressed or changed"
6341                );
6342            }
6343            sqlx::query(
6344                "
6345                INSERT INTO execution_replacement_scan (
6346                    intent_id, chain_id, wallet_address, nonce,
6347                    finalized_cursor_number, finalized_cursor_hash, manifest_digest
6348                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
6349                ON CONFLICT (intent_id) DO UPDATE SET
6350                    finalized_cursor_number = EXCLUDED.finalized_cursor_number,
6351                    finalized_cursor_hash = EXCLUDED.finalized_cursor_hash,
6352                    updated_at = NOW()
6353                ",
6354            )
6355            .bind(scan.intent_id)
6356            .bind(chain_id)
6357            .bind(scan.wallet_address)
6358            .bind(nonce)
6359            .bind(number)
6360            .bind(&cursor.hash)
6361            .bind(scan.manifest_digest)
6362            .execute(&mut *transaction)
6363            .await
6364            .context("failed to persist replacement scan cursor")?;
6365        }
6366
6367        let attempt = sqlx::query_scalar::<_, i64>(
6368            "
6369            SELECT COUNT(*)
6370            FROM execution_verification_decision
6371            WHERE intent_id = $1 AND decision_class = 'replacement_scan'
6372            ",
6373        )
6374        .bind(scan.intent_id)
6375        .fetch_one(&mut *transaction)
6376        .await
6377        .context("failed to number replacement scan evidence")?;
6378        for (index, decision) in scan.decisions.iter().enumerate() {
6379            let height_start = decision
6380                .height_start
6381                .map(i64::try_from)
6382                .transpose()
6383                .context("Replacement scan height exceeds PostgreSQL BIGINT")?;
6384            let height_end = decision
6385                .height_end
6386                .map(i64::try_from)
6387                .transpose()
6388                .context("Replacement scan height exceeds PostgreSQL BIGINT")?;
6389            let transition_key = format!(
6390                "replacement_scan:{}:{attempt}:{}:{index}",
6391                scan.intent_id, decision.read_class
6392            );
6393            sqlx::query(
6394                "
6395                INSERT INTO execution_verification_decision (
6396                    intent_id, nonce, decision_class, read_class, height_start, height_end,
6397                    manifest_version, manifest_digest, provider_ids, operator_ids,
6398                    failure_domain_ids, response_class, normalized_value_digest,
6399                    nonce_revision, outcome, transition_key
6400                ) VALUES (
6401                    $1, $2, 'replacement_scan', $3, $4, $5, $6, $7, $8, $9, $10,
6402                    'all_valid', $11, $12, 'verified', $13
6403                )
6404                ",
6405            )
6406            .bind(scan.intent_id)
6407            .bind(nonce)
6408            .bind(decision.read_class)
6409            .bind(height_start)
6410            .bind(height_end)
6411            .bind(scan.manifest_version)
6412            .bind(scan.manifest_digest)
6413            .bind(scan.provider_ids)
6414            .bind(scan.operator_ids)
6415            .bind(scan.failure_domain_ids)
6416            .bind(&decision.normalized_value_digest)
6417            .bind(revision)
6418            .bind(transition_key)
6419            .execute(&mut *transaction)
6420            .await
6421            .context("failed to persist replacement scan evidence")?;
6422        }
6423
6424        if let Some(transaction_hash) = scan.matched_transaction_hash {
6425            anyhow::ensure!(
6426                execution_transition_allowed(&current_status, TransactionStatus::Replaced),
6427                "Intent cannot attach a verified replacement from status {current_status}"
6428            );
6429            let (hash_id, payload_expected, already_current) =
6430                sqlx::query_as::<_, (i64, bool, bool)>(
6431                    "
6432                    SELECT id, payload_expected, current
6433                    FROM execution_transaction_hash
6434                    WHERE intent_id = $1 AND chain_id = $2 AND transaction_hash = $3
6435                    FOR UPDATE
6436                    ",
6437                )
6438                .bind(scan.intent_id)
6439                .bind(chain_id)
6440                .bind(transaction_hash)
6441                .fetch_optional(&mut *transaction)
6442                .await
6443                .context("failed to lock authenticated replacement payload")?
6444                .ok_or_else(|| {
6445                    anyhow::anyhow!(
6446                        "Replacement transaction {transaction_hash} has no retained payload"
6447                    )
6448                })?;
6449            anyhow::ensure!(
6450                payload_expected,
6451                "Replacement transaction {transaction_hash} is not authenticated"
6452            );
6453
6454            if !already_current {
6455                sqlx::query(
6456                    "
6457                    UPDATE execution_transaction_hash
6458                    SET current = FALSE, status = 'replaced', updated_at = NOW()
6459                    WHERE intent_id = $1 AND current
6460                    ",
6461                )
6462                .bind(scan.intent_id)
6463                .execute(&mut *transaction)
6464                .await
6465                .context("failed to retire the prior transaction hash")?;
6466                sqlx::query(
6467                    "
6468                    UPDATE execution_transaction_hash
6469                    SET current = TRUE, status = 'replaced', updated_at = NOW()
6470                    WHERE id = $1
6471                    ",
6472                )
6473                .bind(hash_id)
6474                .execute(&mut *transaction)
6475                .await
6476                .context("failed to attach the authenticated replacement hash")?;
6477                sqlx::query(
6478                    "UPDATE execution_intent SET status = 'replaced', updated_at = NOW() WHERE id = $1",
6479                )
6480                .bind(scan.intent_id)
6481                .execute(&mut *transaction)
6482                .await
6483                .context("failed to mark the execution intent replaced")?;
6484                sqlx::query(
6485                    "
6486                    INSERT INTO execution_transaction_transition (
6487                        intent_id, transaction_hash_id, transition_key, from_status, to_status
6488                    ) VALUES ($1, $2, $3, $4, 'replaced')
6489                    ON CONFLICT (intent_id, transition_key) DO NOTHING
6490                    ",
6491                )
6492                .bind(scan.intent_id)
6493                .bind(hash_id)
6494                .bind(format!("replacement_verified:{transaction_hash}"))
6495                .bind(current_status)
6496                .execute(&mut *transaction)
6497                .await
6498                .context("failed to record the verified replacement transition")?;
6499            }
6500        }
6501
6502        transaction
6503            .commit()
6504            .await
6505            .context("failed to commit verified replacement scan")?;
6506        Ok(())
6507    }
6508
6509    /// Releases an intent when no broadcast attempt can have occurred.
6510    ///
6511    /// # Errors
6512    ///
6513    /// Returns an error if the intent advanced to broadcast or persistence fails.
6514    pub async fn mark_execution_intent_recoverable(&self, intent_id: i64) -> anyhow::Result<()> {
6515        let mut transaction = self.pool.begin().await.map_err(|e| {
6516            anyhow::anyhow!("Failed to start recoverable execution transition: {e}")
6517        })?;
6518        let current_status = sqlx::query_scalar::<_, String>(
6519            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
6520        )
6521        .bind(intent_id)
6522        .fetch_optional(&mut *transaction)
6523        .await
6524        .map_err(|e| anyhow::anyhow!("Failed to lock recoverable execution intent: {e}"))?
6525        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
6526        anyhow::ensure!(
6527            current_status == "prepared",
6528            "Execution intent {intent_id} is {current_status}, not recoverable before signing"
6529        );
6530        let result = sqlx::query(
6531            "
6532            UPDATE execution_intent
6533            SET status = 'recoverable', active = FALSE, updated_at = NOW()
6534            WHERE id = $1 AND status = 'prepared'
6535            ",
6536        )
6537        .bind(intent_id)
6538        .execute(&mut *transaction)
6539        .await
6540        .map_err(|e| anyhow::anyhow!("Failed to mark execution intent recoverable: {e}"))?;
6541        anyhow::ensure!(
6542            result.rows_affected() == 1,
6543            "Execution intent {intent_id} is not recoverable from preparation"
6544        );
6545        sqlx::query(
6546            "
6547            INSERT INTO execution_transaction_transition (
6548                intent_id, transition_key, from_status, to_status
6549            ) VALUES ($1, 'recoverable', $2, 'recoverable')
6550            ON CONFLICT (intent_id, transition_key) DO NOTHING
6551            ",
6552        )
6553        .bind(intent_id)
6554        .bind(current_status)
6555        .execute(&mut *transaction)
6556        .await
6557        .map_err(|e| anyhow::anyhow!("Failed to record recoverable transition: {e}"))?;
6558        transaction
6559            .commit()
6560            .await
6561            .map_err(|e| anyhow::anyhow!("Failed to commit recoverable transition: {e}"))?;
6562        Ok(())
6563    }
6564
6565    /// Persists a signed transaction and advances its intent before broadcast.
6566    ///
6567    /// # Errors
6568    ///
6569    /// Returns an error if the intent is not prepared, lacks a nonce, conflicts with a stored
6570    /// hash, or persistence fails.
6571    pub async fn add_execution_transaction_hash(
6572        &self,
6573        intent_id: i64,
6574        chain_id: u32,
6575        transaction_hash: &str,
6576        raw_transaction: &[u8],
6577    ) -> anyhow::Result<ExecutionTransactionHashRow> {
6578        self.add_execution_transaction_payload(
6579            intent_id,
6580            chain_id,
6581            transaction_hash,
6582            Some(raw_transaction),
6583            None,
6584        )
6585        .await
6586    }
6587
6588    pub(crate) async fn add_execution_transaction_envelope(
6589        &self,
6590        intent_id: i64,
6591        chain_id: u32,
6592        transaction_hash: &str,
6593        sealed_transaction: &[u8],
6594    ) -> anyhow::Result<ExecutionTransactionHashRow> {
6595        self.add_execution_transaction_payload(
6596            intent_id,
6597            chain_id,
6598            transaction_hash,
6599            None,
6600            Some(sealed_transaction),
6601        )
6602        .await
6603    }
6604
6605    async fn add_execution_transaction_payload(
6606        &self,
6607        intent_id: i64,
6608        chain_id: u32,
6609        transaction_hash: &str,
6610        raw_transaction: Option<&[u8]>,
6611        sealed_transaction: Option<&[u8]>,
6612    ) -> anyhow::Result<ExecutionTransactionHashRow> {
6613        anyhow::ensure!(
6614            raw_transaction.is_some() != sealed_transaction.is_some(),
6615            "A signed transaction requires exactly one payload representation"
6616        );
6617        let chain_id_db = i32::try_from(chain_id)
6618            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
6619        let mut transaction =
6620            self.pool.begin().await.map_err(|e| {
6621                anyhow::anyhow!("Failed to start signed transaction persistence: {e}")
6622            })?;
6623
6624        if let Some(envelope) = sealed_transaction {
6625            let state_row = sqlx::query(
6626                "SELECT deployment_id, protocol_version, operation, active_key_id \
6627                 FROM execution_payload_state WHERE component = 'signed_transactions' FOR SHARE",
6628            )
6629            .fetch_optional(&mut *transaction)
6630            .await
6631            .context("failed to lock execution payload state for protected persistence")?
6632            .ok_or_else(|| anyhow::anyhow!("Execution payload protection is not active"))?;
6633            let state = execution_payload_state_from_row(&state_row)?;
6634            anyhow::ensure!(
6635                state.protocol_version == EXECUTION_PAYLOAD_PROTOCOL_VERSION
6636                    && state.operation == "ready",
6637                "Execution payload storage is not ready for protected persistence"
6638            );
6639            anyhow::ensure!(
6640                envelope_key_id(envelope)?.as_slice() == state.active_key_id.as_slice(),
6641                "Signed transaction envelope does not use the database active key"
6642            );
6643        } else {
6644            let marker = sqlx::query_scalar::<_, bool>(
6645                "SELECT EXISTS (SELECT 1 FROM execution_schema_version WHERE component = $1)",
6646            )
6647            .bind(EXECUTION_PAYLOAD_COMPONENT)
6648            .fetch_one(&mut *transaction)
6649            .await
6650            .context("failed to inspect execution payload marker")?;
6651            anyhow::ensure!(
6652                !marker,
6653                "Plaintext signed transaction persistence is disabled after payload activation"
6654            );
6655        }
6656        let current_status = sqlx::query_scalar::<_, String>(
6657            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
6658        )
6659        .bind(intent_id)
6660        .fetch_optional(&mut *transaction)
6661        .await
6662        .map_err(|e| anyhow::anyhow!("Failed to lock execution intent {intent_id}: {e}"))?
6663        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
6664        anyhow::ensure!(
6665            current_status == TransactionStatus::Prepared.as_str()
6666                || current_status == TransactionStatus::Signed.as_str(),
6667            "Execution intent {intent_id} is {current_status}, not prepared for signing"
6668        );
6669
6670        let row = sqlx::query_as::<_, ExecutionTransactionHashRow>(
6671            "
6672            INSERT INTO execution_transaction_hash (
6673                intent_id, chain_id, transaction_hash, payload_expected,
6674                raw_transaction, sealed_transaction, status
6675            )
6676            SELECT id, chain_id, $3, TRUE, $4, $5, 'signed'
6677            FROM execution_intent
6678            WHERE id = $1 AND chain_id = $2 AND nonce IS NOT NULL
6679            ON CONFLICT (chain_id, transaction_hash) DO UPDATE
6680            SET transaction_hash = EXCLUDED.transaction_hash
6681            WHERE execution_transaction_hash.intent_id = EXCLUDED.intent_id
6682              AND execution_transaction_hash.payload_expected
6683              AND (
6684                  (
6685                      EXCLUDED.raw_transaction IS NOT NULL
6686                      AND execution_transaction_hash.raw_transaction = EXCLUDED.raw_transaction
6687                      AND execution_transaction_hash.sealed_transaction IS NULL
6688                  )
6689                  OR (
6690                      EXCLUDED.sealed_transaction IS NOT NULL
6691                      AND execution_transaction_hash.raw_transaction IS NULL
6692                      AND execution_transaction_hash.sealed_transaction IS NOT NULL
6693                  )
6694              )
6695            RETURNING
6696                id, intent_id, chain_id, transaction_hash, payload_expected,
6697                raw_transaction, sealed_transaction, status,
6698                block_number, block_hash, receipt_success, gas_used,
6699                effective_gas_price, current
6700            ",
6701        )
6702        .bind(intent_id)
6703        .bind(chain_id_db)
6704        .bind(transaction_hash)
6705        .bind(raw_transaction)
6706        .bind(sealed_transaction)
6707        .fetch_optional(&mut *transaction)
6708        .await
6709        .map_err(|e| {
6710            anyhow::anyhow!("Failed to persist signed transaction {transaction_hash}: {e}")
6711        })?
6712        .ok_or_else(|| {
6713            anyhow::anyhow!(
6714                "Signed transaction {transaction_hash} conflicts with its persisted identity"
6715            )
6716        })?;
6717
6718        sqlx::query(
6719            "UPDATE execution_intent SET status = 'signed', updated_at = NOW() WHERE id = $1",
6720        )
6721        .bind(intent_id)
6722        .execute(&mut *transaction)
6723        .await
6724        .map_err(|e| anyhow::anyhow!("Failed to mark execution intent signed: {e}"))?;
6725        sqlx::query(
6726            "
6727            INSERT INTO execution_transaction_transition (
6728                intent_id, transaction_hash_id, transition_key, from_status, to_status
6729            ) VALUES ($1, $2, $3, $4, 'signed')
6730            ON CONFLICT (intent_id, transition_key) DO NOTHING
6731            ",
6732        )
6733        .bind(intent_id)
6734        .bind(row.id)
6735        .bind(format!("signed:{transaction_hash}"))
6736        .bind(current_status)
6737        .execute(&mut *transaction)
6738        .await
6739        .map_err(|e| anyhow::anyhow!("Failed to record signed transaction transition: {e}"))?;
6740
6741        transaction
6742            .commit()
6743            .await
6744            .map_err(|e| anyhow::anyhow!("Failed to commit signed transaction: {e}"))?;
6745        Ok(row)
6746    }
6747
6748    /// Records an idempotent transaction observation and advances its intent state.
6749    ///
6750    /// # Errors
6751    ///
6752    /// Returns an error if the transition is invalid, the hash is unknown, or persistence fails.
6753    #[expect(
6754        clippy::too_many_arguments,
6755        reason = "the parameters are the canonical receipt observation persisted atomically"
6756    )]
6757    pub async fn record_execution_status(
6758        &self,
6759        intent_id: i64,
6760        transaction_hash: &str,
6761        status: TransactionStatus,
6762        block_number: Option<u64>,
6763        block_hash: Option<&str>,
6764        receipt_success: Option<bool>,
6765        gas_used: Option<u64>,
6766        effective_gas_price: Option<&str>,
6767    ) -> anyhow::Result<()> {
6768        let block_number_db = block_number
6769            .map(i64::try_from)
6770            .transpose()
6771            .with_context(|| {
6772                format!(
6773                    "Execution block number {} exceeds PostgreSQL BIGINT",
6774                    block_number.unwrap_or_default()
6775                )
6776            })?;
6777        let gas_used_db = gas_used.map(i64::try_from).transpose().with_context(|| {
6778            format!(
6779                "Execution gas used {} exceeds PostgreSQL BIGINT",
6780                gas_used.unwrap_or_default()
6781            )
6782        })?;
6783        let mut transaction = self
6784            .pool
6785            .begin()
6786            .await
6787            .map_err(|e| anyhow::anyhow!("Failed to start execution status transition: {e}"))?;
6788        let (current_status, fill_emitted, terminal_emitted) =
6789            sqlx::query_as::<_, (String, bool, bool)>(
6790            "SELECT status, fill_emitted, terminal_emitted FROM execution_intent WHERE id = $1 FOR UPDATE",
6791        )
6792        .bind(intent_id)
6793        .fetch_optional(&mut *transaction)
6794        .await
6795        .map_err(|e| anyhow::anyhow!("Failed to lock execution intent {intent_id}: {e}"))?
6796        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
6797        anyhow::ensure!(
6798            execution_transition_allowed(&current_status, status),
6799            "Invalid execution transition for intent {intent_id}: {current_status} -> {}",
6800            status.as_str()
6801        );
6802
6803        let active = match status {
6804            TransactionStatus::Finalized | TransactionStatus::Reverted => {
6805                !fill_emitted && !terminal_emitted
6806            }
6807            TransactionStatus::Recoverable => false,
6808            _ => true,
6809        };
6810        let hash_result = sqlx::query(
6811            "
6812            UPDATE execution_transaction_hash
6813            SET status = $3,
6814                block_number = COALESCE($4, block_number),
6815                block_hash = COALESCE($5, block_hash),
6816                receipt_success = COALESCE($6, receipt_success),
6817                gas_used = COALESCE($7, gas_used),
6818                effective_gas_price = COALESCE($8, effective_gas_price),
6819                updated_at = NOW()
6820            WHERE intent_id = $1 AND transaction_hash = $2
6821            ",
6822        )
6823        .bind(intent_id)
6824        .bind(transaction_hash)
6825        .bind(status.as_str())
6826        .bind(block_number_db)
6827        .bind(block_hash)
6828        .bind(receipt_success)
6829        .bind(gas_used_db)
6830        .bind(effective_gas_price)
6831        .execute(&mut *transaction)
6832        .await
6833        .map_err(|e| anyhow::anyhow!("Failed to update execution hash {transaction_hash}: {e}"))?;
6834        anyhow::ensure!(
6835            hash_result.rows_affected() == 1,
6836            "Execution transaction hash {transaction_hash} was not found for intent {intent_id}"
6837        );
6838
6839        sqlx::query(
6840            "
6841            UPDATE execution_intent
6842            SET status = $2, active = $3, updated_at = NOW()
6843            WHERE id = $1
6844            ",
6845        )
6846        .bind(intent_id)
6847        .bind(status.as_str())
6848        .bind(active)
6849        .execute(&mut *transaction)
6850        .await
6851        .map_err(|e| anyhow::anyhow!("Failed to update execution intent {intent_id}: {e}"))?;
6852
6853        let transition_key = format!(
6854            "{}:{transaction_hash}:{}:{}",
6855            status.as_str(),
6856            block_number.map_or_else(|| "none".to_string(), |value| value.to_string()),
6857            block_hash.unwrap_or("none")
6858        );
6859        sqlx::query(
6860            "
6861            INSERT INTO execution_transaction_transition (
6862                intent_id, transaction_hash_id, transition_key, from_status, to_status,
6863                block_number, block_hash
6864            )
6865            SELECT $1, id, $3, $4, $5, $6, $7
6866            FROM execution_transaction_hash
6867            WHERE intent_id = $1 AND transaction_hash = $2
6868            ON CONFLICT (intent_id, transition_key) DO NOTHING
6869            ",
6870        )
6871        .bind(intent_id)
6872        .bind(transaction_hash)
6873        .bind(transition_key)
6874        .bind(current_status)
6875        .bind(status.as_str())
6876        .bind(block_number_db)
6877        .bind(block_hash)
6878        .execute(&mut *transaction)
6879        .await
6880        .map_err(|e| anyhow::anyhow!("Failed to record execution transition: {e}"))?;
6881
6882        transaction
6883            .commit()
6884            .await
6885            .map_err(|e| anyhow::anyhow!("Failed to commit execution transition: {e}"))?;
6886        Ok(())
6887    }
6888
6889    /// Records verified final consumption and advances the canonical nonce in one transaction.
6890    ///
6891    /// # Errors
6892    ///
6893    /// Returns an error if the receipt transition, nonce ledger, manifest identity, or evidence
6894    /// is inconsistent, or if persistence fails.
6895    pub(crate) async fn record_execution_finality_verified(
6896        &self,
6897        finality: &ExecutionFinalityTransition<'_>,
6898    ) -> anyhow::Result<()> {
6899        anyhow::ensure!(
6900            matches!(
6901                finality.status,
6902                TransactionStatus::Finalized | TransactionStatus::Reverted
6903            ),
6904            "Verified finality requires a finalized or reverted status"
6905        );
6906        anyhow::ensure!(
6907            !finality.decisions.is_empty(),
6908            "Verified finality requires decision evidence"
6909        );
6910        anyhow::ensure!(
6911            !finality.finalized_headers.is_empty()
6912                && finality.finalized_headers.windows(2).all(|headers| {
6913                    headers[1].number == headers[0].number.saturating_add(1)
6914                        && headers[1].parent_hash == headers[0].hash
6915                })
6916                && finality
6917                    .finalized_headers
6918                    .last()
6919                    .is_some_and(|header| header.number >= finality.block_number),
6920            "Verified finality headers must form a continuous chain through the inclusion height"
6921        );
6922        let chain_id = i32::try_from(finality.chain_id)
6923            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
6924        let nonce =
6925            i64::try_from(finality.nonce).context("Execution nonce exceeds PostgreSQL BIGINT")?;
6926        let next_nonce = nonce
6927            .checked_add(1)
6928            .ok_or_else(|| anyhow::anyhow!("Canonical nonce overflow"))?;
6929        let block_number = i64::try_from(finality.block_number)
6930            .context("Finality block exceeds PostgreSQL BIGINT")?;
6931        let gas_used = i64::try_from(finality.gas_used)
6932            .context("Finality gas used exceeds PostgreSQL BIGINT")?;
6933        let mut transaction =
6934            self.pool.begin().await.map_err(|e| {
6935                anyhow::anyhow!("Failed to start verified finality transition: {e}")
6936            })?;
6937        let (manifest_version, manifest_digest, stored_nonce, revision) =
6938            sqlx::query_as::<_, (String, String, i64, i64)>(
6939                "
6940                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
6941                FROM execution_verification_nonce
6942                WHERE chain_id = $1 AND wallet_address = $2
6943                FOR UPDATE
6944                ",
6945            )
6946            .bind(chain_id)
6947            .bind(finality.wallet_address)
6948            .fetch_optional(&mut *transaction)
6949            .await
6950            .map_err(|e| anyhow::anyhow!("Failed to lock finality nonce ledger: {e}"))?
6951            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
6952        anyhow::ensure!(
6953            manifest_version == finality.manifest_version
6954                && manifest_digest == finality.manifest_digest,
6955            "Verified finality manifest identity changed"
6956        );
6957        anyhow::ensure!(
6958            stored_nonce == nonce,
6959            "Finalized nonce {} does not match canonical nonce {stored_nonce}",
6960            finality.nonce
6961        );
6962        let stored_tip = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
6963            "
6964            SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
6965            FROM execution_verified_finalized_header
6966            WHERE chain_id = $1 AND wallet_address = $2
6967            ORDER BY number DESC
6968            LIMIT 1
6969            ",
6970        )
6971        .bind(chain_id)
6972        .bind(finality.wallet_address)
6973        .fetch_one(&mut *transaction)
6974        .await
6975        .map_err(|e| anyhow::anyhow!("Failed to lock finalized header tip: {e}"))?;
6976        let first_header = &finality.finalized_headers[0];
6977        anyhow::ensure!(
6978            stored_tip
6979                == (
6980                    i64::try_from(first_header.number)
6981                        .context("Verified finalized height exceeds PostgreSQL BIGINT")?,
6982                    first_header.hash.clone(),
6983                    first_header.parent_hash.clone(),
6984                    i64::try_from(first_header.timestamp)
6985                        .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?,
6986                    first_header.base_fee_per_gas.map(|value| value.to_string()),
6987                    finality.manifest_digest.to_string(),
6988                ),
6989            "Verified finality extension does not start at the durable tip"
6990        );
6991
6992        for header in finality.finalized_headers.iter().skip(1) {
6993            let number = i64::try_from(header.number)
6994                .context("Verified finalized height exceeds PostgreSQL BIGINT")?;
6995            let timestamp = i64::try_from(header.timestamp)
6996                .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?;
6997            let base_fee = header.base_fee_per_gas.map(|value| value.to_string());
6998            sqlx::query(
6999                "
7000                INSERT INTO execution_verified_finalized_header (
7001                    chain_id, wallet_address, number, hash, parent_hash, timestamp,
7002                    base_fee_per_gas, manifest_digest
7003                )
7004                VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
7005                ON CONFLICT (chain_id, wallet_address, number) DO NOTHING
7006                ",
7007            )
7008            .bind(chain_id)
7009            .bind(finality.wallet_address)
7010            .bind(number)
7011            .bind(&header.hash)
7012            .bind(&header.parent_hash)
7013            .bind(timestamp)
7014            .bind(&base_fee)
7015            .bind(finality.manifest_digest)
7016            .execute(&mut *transaction)
7017            .await
7018            .map_err(|e| anyhow::anyhow!("Failed to extend finalized header ledger: {e}"))?;
7019            let stored = sqlx::query_as::<_, (String, String, i64, Option<String>, String)>(
7020                "
7021                SELECT hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
7022                FROM execution_verified_finalized_header
7023                WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
7024                ",
7025            )
7026            .bind(chain_id)
7027            .bind(finality.wallet_address)
7028            .bind(number)
7029            .fetch_one(&mut *transaction)
7030            .await
7031            .map_err(|e| anyhow::anyhow!("Failed to validate finalized header ledger: {e}"))?;
7032            anyhow::ensure!(
7033                stored
7034                    == (
7035                        header.hash.clone(),
7036                        header.parent_hash.clone(),
7037                        timestamp,
7038                        base_fee,
7039                        finality.manifest_digest.to_string(),
7040                    ),
7041                "Finalized header ledger conflicts at height {}",
7042                header.number
7043            );
7044        }
7045
7046        let (current_status, intent_nonce, fill_emitted, terminal_emitted) =
7047            sqlx::query_as::<_, (String, Option<i64>, bool, bool)>(
7048                "
7049                SELECT status, nonce, fill_emitted, terminal_emitted
7050                FROM execution_intent
7051                WHERE id = $1 AND chain_id = $2 AND wallet_address = $3 AND active
7052                FOR UPDATE
7053                ",
7054            )
7055            .bind(finality.intent_id)
7056            .bind(chain_id)
7057            .bind(finality.wallet_address)
7058            .fetch_optional(&mut *transaction)
7059            .await
7060            .map_err(|e| anyhow::anyhow!("Failed to lock intent for verified finality: {e}"))?
7061            .ok_or_else(|| anyhow::anyhow!("Active finality intent was not found"))?;
7062        anyhow::ensure!(
7063            intent_nonce == Some(nonce)
7064                && execution_transition_allowed(&current_status, finality.status),
7065            "Intent cannot make the verified finality transition"
7066        );
7067
7068        for (index, decision) in finality.decisions.iter().enumerate() {
7069            let height_start = decision
7070                .height_start
7071                .map(i64::try_from)
7072                .transpose()
7073                .context("Verification height exceeds PostgreSQL BIGINT")?;
7074            let height_end = decision
7075                .height_end
7076                .map(i64::try_from)
7077                .transpose()
7078                .context("Verification height exceeds PostgreSQL BIGINT")?;
7079            let transition_key = format!(
7080                "finality:{}:{}:{index}",
7081                finality.intent_id, decision.read_class
7082            );
7083            sqlx::query(
7084                "
7085                INSERT INTO execution_verification_decision (
7086                    intent_id, nonce, decision_class, read_class, height_start, height_end,
7087                    manifest_version, manifest_digest, provider_ids, operator_ids,
7088                    failure_domain_ids, response_class, normalized_value_digest,
7089                    nonce_revision, outcome, transition_key
7090                )
7091                VALUES (
7092                    $1, $2, 'finality', $3, $4, $5, $6, $7, $8, $9, $10,
7093                    'all_valid', $11, $12, 'verified', $13
7094                )
7095                ",
7096            )
7097            .bind(finality.intent_id)
7098            .bind(nonce)
7099            .bind(decision.read_class)
7100            .bind(height_start)
7101            .bind(height_end)
7102            .bind(finality.manifest_version)
7103            .bind(finality.manifest_digest)
7104            .bind(finality.provider_ids)
7105            .bind(finality.operator_ids)
7106            .bind(finality.failure_domain_ids)
7107            .bind(&decision.normalized_value_digest)
7108            .bind(revision)
7109            .bind(transition_key)
7110            .execute(&mut *transaction)
7111            .await
7112            .map_err(|e| anyhow::anyhow!("Failed to persist finality verification: {e}"))?;
7113        }
7114
7115        let hash_result = sqlx::query(
7116            "
7117            UPDATE execution_transaction_hash
7118            SET status = $3, block_number = $4, block_hash = $5,
7119                receipt_success = $6, gas_used = $7, effective_gas_price = $8,
7120                updated_at = NOW()
7121            WHERE intent_id = $1 AND transaction_hash = $2
7122            ",
7123        )
7124        .bind(finality.intent_id)
7125        .bind(finality.transaction_hash)
7126        .bind(finality.status.as_str())
7127        .bind(block_number)
7128        .bind(finality.block_hash)
7129        .bind(finality.receipt_success)
7130        .bind(gas_used)
7131        .bind(finality.effective_gas_price)
7132        .execute(&mut *transaction)
7133        .await
7134        .map_err(|e| anyhow::anyhow!("Failed to record verified finality receipt: {e}"))?;
7135        anyhow::ensure!(
7136            hash_result.rows_affected() == 1,
7137            "Finality transaction hash was not found"
7138        );
7139
7140        let active = !fill_emitted && !terminal_emitted;
7141        sqlx::query(
7142            "UPDATE execution_intent SET status = $2, active = $3, updated_at = NOW() WHERE id = $1",
7143        )
7144        .bind(finality.intent_id)
7145        .bind(finality.status.as_str())
7146        .bind(active)
7147        .execute(&mut *transaction)
7148        .await
7149        .map_err(|e| anyhow::anyhow!("Failed to record verified finality intent: {e}"))?;
7150
7151        let transition_key = format!(
7152            "{}:{}:{}:{}",
7153            finality.status.as_str(),
7154            finality.transaction_hash,
7155            finality.block_number,
7156            finality.block_hash
7157        );
7158        sqlx::query(
7159            "
7160            INSERT INTO execution_transaction_transition (
7161                intent_id, transaction_hash_id, transition_key, from_status, to_status,
7162                block_number, block_hash
7163            )
7164            SELECT $1, id, $3, $4, $5, $6, $7
7165            FROM execution_transaction_hash
7166            WHERE intent_id = $1 AND transaction_hash = $2
7167            ",
7168        )
7169        .bind(finality.intent_id)
7170        .bind(finality.transaction_hash)
7171        .bind(transition_key)
7172        .bind(current_status)
7173        .bind(finality.status.as_str())
7174        .bind(block_number)
7175        .bind(finality.block_hash)
7176        .execute(&mut *transaction)
7177        .await
7178        .map_err(|e| anyhow::anyhow!("Failed to record verified finality transition: {e}"))?;
7179
7180        let nonce_result = sqlx::query(
7181            "
7182            UPDATE execution_verification_nonce
7183            SET next_canonical_nonce = $3, revision = revision + 1, updated_at = NOW()
7184            WHERE chain_id = $1 AND wallet_address = $2
7185              AND next_canonical_nonce = $4 AND revision = $5
7186            ",
7187        )
7188        .bind(chain_id)
7189        .bind(finality.wallet_address)
7190        .bind(next_nonce)
7191        .bind(nonce)
7192        .bind(revision)
7193        .execute(&mut *transaction)
7194        .await
7195        .map_err(|e| anyhow::anyhow!("Failed to advance canonical nonce ledger: {e}"))?;
7196        anyhow::ensure!(
7197            nonce_result.rows_affected() == 1,
7198            "Canonical nonce ledger changed during finality transition"
7199        );
7200        transaction
7201            .commit()
7202            .await
7203            .map_err(|e| anyhow::anyhow!("Failed to commit verified finality transition: {e}"))?;
7204        Ok(())
7205    }
7206
7207    /// Loads one durable execution intent by ID.
7208    pub(crate) async fn get_execution_intent(
7209        &self,
7210        intent_id: i64,
7211    ) -> anyhow::Result<ExecutionIntentRow> {
7212        sqlx::query_as::<_, ExecutionIntentRow>(
7213            "
7214            SELECT
7215                id, schema_version, chain_id, wallet_address, nonce, purpose, status,
7216                client_order_id, trader_id, strategy_id, account_id, instrument_id,
7217                pool_address, transaction_to, transaction_input, transaction_value,
7218                amount_in, created_block, acknowledgement_emitted, fill_emitted,
7219                terminal_emitted, active
7220            FROM execution_intent
7221            WHERE id = $1
7222            ",
7223        )
7224        .bind(intent_id)
7225        .fetch_optional(&self.pool)
7226        .await
7227        .context("failed to load execution intent")?
7228        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))
7229    }
7230
7231    /// Loads the active intent owned by a signer after any concurrent reservation completes.
7232    ///
7233    /// # Errors
7234    ///
7235    /// Returns an error if the query fails.
7236    pub async fn get_active_execution_intent(
7237        &self,
7238        chain_id: u32,
7239        wallet_address: &str,
7240    ) -> anyhow::Result<Option<ExecutionIntentRow>> {
7241        let chain_id_db = i32::try_from(chain_id)
7242            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
7243        let mut transaction = self
7244            .pool
7245            .begin_with("BEGIN ISOLATION LEVEL READ COMMITTED")
7246            .await
7247            .context("failed to start active execution intent reconciliation")?;
7248        Self::lock_execution_signer(&mut transaction, chain_id, wallet_address).await?;
7249        let intent = sqlx::query_as::<_, ExecutionIntentRow>(
7250            "
7251            SELECT
7252                id, schema_version, chain_id, wallet_address, nonce, purpose, status,
7253                client_order_id, trader_id, strategy_id, account_id, instrument_id,
7254                pool_address, transaction_to, transaction_input, transaction_value,
7255                amount_in, created_block, acknowledgement_emitted, fill_emitted,
7256                terminal_emitted, active
7257            FROM execution_intent
7258            WHERE chain_id = $1 AND wallet_address = $2 AND active
7259            ",
7260        )
7261        .bind(chain_id_db)
7262        .bind(wallet_address)
7263        .fetch_optional(&mut *transaction)
7264        .await
7265        .context("failed to load active execution intent")?;
7266        transaction
7267            .commit()
7268            .await
7269            .context("failed to complete active execution intent reconciliation")?;
7270        Ok(intent)
7271    }
7272
7273    async fn lock_execution_signer(
7274        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
7275        chain_id: u32,
7276        wallet_address: &str,
7277    ) -> anyhow::Result<()> {
7278        let lock = PgAdvisoryLock::new(format!(
7279            "nautilus:blockchain:execution:{chain_id}:{}",
7280            wallet_address.to_ascii_lowercase()
7281        ));
7282        let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
7283            unreachable!("string advisory locks use the 64-bit key space");
7284        };
7285
7286        // Transaction-scoped release makes row presence or absence authoritative to the next
7287        // holder after an ambiguous reservation commit.
7288        sqlx::query("SELECT pg_advisory_xact_lock($1)")
7289            .bind(*lock_key)
7290            .execute(&mut **transaction)
7291            .await
7292            .context("failed to acquire execution signer reservation fence")?;
7293        Ok(())
7294    }
7295
7296    /// Reports whether a released legacy intent still retains a broadcastable signature.
7297    ///
7298    /// # Errors
7299    ///
7300    /// Returns an error if the query fails.
7301    pub async fn has_recoverable_signed_execution(
7302        &self,
7303        chain_id: u32,
7304        wallet_address: &str,
7305    ) -> anyhow::Result<bool> {
7306        let chain_id_db = i32::try_from(chain_id)
7307            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
7308        sqlx::query_scalar::<_, bool>(
7309            "
7310            SELECT EXISTS (
7311                SELECT 1
7312                FROM execution_intent AS intent
7313                JOIN execution_transaction_hash AS hash ON hash.intent_id = intent.id
7314                WHERE intent.chain_id = $1
7315                  AND intent.wallet_address = $2
7316                  AND intent.status = 'recoverable'
7317                  AND (hash.raw_transaction IS NOT NULL OR hash.sealed_transaction IS NOT NULL)
7318            )
7319            ",
7320        )
7321        .bind(chain_id_db)
7322        .bind(wallet_address)
7323        .fetch_one(&self.pool)
7324        .await
7325        .map_err(|e| anyhow::anyhow!("Failed to inspect recoverable signed executions: {e}"))
7326    }
7327
7328    /// Loads all transaction hashes for an intent in insertion order.
7329    ///
7330    /// # Errors
7331    ///
7332    /// Returns an error if the query fails.
7333    pub async fn get_execution_transaction_hashes(
7334        &self,
7335        intent_id: i64,
7336    ) -> anyhow::Result<Vec<ExecutionTransactionHashRow>> {
7337        sqlx::query_as::<_, ExecutionTransactionHashRow>(
7338            "
7339            SELECT
7340                id, intent_id, chain_id, transaction_hash, payload_expected,
7341                raw_transaction, sealed_transaction, status,
7342                block_number, block_hash, receipt_success, gas_used,
7343                effective_gas_price, current
7344            FROM execution_transaction_hash
7345            WHERE intent_id = $1
7346            ORDER BY id
7347            ",
7348        )
7349        .bind(intent_id)
7350        .fetch_all(&self.pool)
7351        .await
7352        .map_err(|e| anyhow::anyhow!("Failed to load execution transaction hashes: {e}"))
7353    }
7354
7355    /// Marks one order event as emitted after dispatch.
7356    ///
7357    /// # Errors
7358    ///
7359    /// Returns an error if the event kind is unknown, the intent is absent, the opposing
7360    /// terminal marker is already set, or persistence fails.
7361    pub async fn mark_execution_event_emitted(
7362        &self,
7363        intent_id: i64,
7364        event: &str,
7365    ) -> anyhow::Result<()> {
7366        let statement = match event {
7367            "acknowledgement" => {
7368                "UPDATE execution_intent SET acknowledgement_emitted = TRUE, updated_at = NOW() WHERE id = $1"
7369            }
7370            "fill" => {
7371                "UPDATE execution_intent SET fill_emitted = TRUE, active = CASE WHEN status = 'finalized' THEN FALSE ELSE active END, updated_at = NOW() WHERE id = $1 AND NOT terminal_emitted"
7372            }
7373            "terminal" => {
7374                "UPDATE execution_intent SET terminal_emitted = TRUE, active = CASE WHEN status IN ('finalized', 'reverted') THEN FALSE ELSE active END, updated_at = NOW() WHERE id = $1 AND NOT fill_emitted"
7375            }
7376            _ => anyhow::bail!("Unknown execution event marker {event}"),
7377        };
7378        let result = sqlx::query(statement)
7379            .bind(intent_id)
7380            .execute(&self.pool)
7381            .await
7382            .map_err(|e| anyhow::anyhow!("Failed to mark execution {event} emitted: {e}"))?;
7383        anyhow::ensure!(
7384            result.rows_affected() == 1,
7385            "Execution intent {intent_id} cannot mark {event} emitted"
7386        );
7387        Ok(())
7388    }
7389
7390    /// Updates the status of a persisted execution transaction record.
7391    ///
7392    /// # Errors
7393    ///
7394    /// Returns an error if the database operation fails.
7395    pub async fn update_execution_transaction_status(
7396        &self,
7397        chain_id: u32,
7398        transaction_hash: &str,
7399        status: &str,
7400    ) -> anyhow::Result<()> {
7401        let result = sqlx::query(
7402            "
7403            UPDATE execution_transaction
7404            SET status = $3
7405            WHERE chain_id = $1 AND transaction_hash = $2
7406        ",
7407        )
7408        .bind(chain_id as i32)
7409        .bind(transaction_hash)
7410        .bind(status)
7411        .execute(&self.pool)
7412        .await
7413        .map_err(|e| anyhow::anyhow!("Failed to update execution_transaction table: {e}"))?;
7414
7415        anyhow::ensure!(
7416            result.rows_affected() == 1,
7417            "Execution transaction {transaction_hash} was not found for status update"
7418        );
7419        Ok(())
7420    }
7421
7422    /// Loads an execution transaction record by chain ID and transaction hash.
7423    ///
7424    /// # Errors
7425    ///
7426    /// Returns an error if the database operation fails.
7427    pub async fn get_execution_transaction(
7428        &self,
7429        chain_id: u32,
7430        transaction_hash: &str,
7431    ) -> anyhow::Result<Option<ExecutionTransactionRow>> {
7432        let chain_id_db = i32::try_from(chain_id)
7433            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
7434        sqlx::query_as::<_, ExecutionTransactionRow>(
7435            "
7436            SELECT wallet_address, nonce, transaction_hash, purpose, status, client_order_id
7437            FROM (
7438                SELECT
7439                    intent.wallet_address,
7440                    intent.nonce,
7441                    hash.transaction_hash,
7442                    intent.purpose,
7443                    intent.status,
7444                    intent.client_order_id,
7445                    0 AS source_priority
7446                FROM execution_transaction_hash AS hash
7447                JOIN execution_intent AS intent ON intent.id = hash.intent_id
7448                WHERE hash.chain_id = $1 AND hash.transaction_hash = $2
7449                UNION ALL
7450                SELECT
7451                    wallet_address,
7452                    nonce,
7453                    transaction_hash,
7454                    purpose,
7455                    status,
7456                    client_order_id,
7457                    1 AS source_priority
7458                FROM execution_transaction
7459                WHERE chain_id = $1 AND transaction_hash = $2
7460            ) AS record
7461            ORDER BY source_priority
7462            LIMIT 1
7463        ",
7464        )
7465        .bind(chain_id_db)
7466        .bind(transaction_hash)
7467        .fetch_optional(&self.pool)
7468        .await
7469        .map_err(|e| anyhow::anyhow!("Failed to load from execution_transaction table: {e}"))
7470    }
7471}
7472
7473fn execution_payload_state_from_row(
7474    row: &sqlx::postgres::PgRow,
7475) -> anyhow::Result<ExecutionPayloadState> {
7476    Ok(ExecutionPayloadState {
7477        deployment_id: row.try_get("deployment_id")?,
7478        protocol_version: row.try_get("protocol_version")?,
7479        operation: row.try_get("operation")?,
7480        active_key_id: row.try_get("active_key_id")?,
7481    })
7482}
7483
7484fn validate_execution_payload_state(
7485    state: &ExecutionPayloadState,
7486    keys: &PayloadKeySet,
7487) -> anyhow::Result<()> {
7488    anyhow::ensure!(
7489        state.protocol_version == EXECUTION_PAYLOAD_PROTOCOL_VERSION,
7490        "Execution payload protocol version {} is not supported",
7491        state.protocol_version
7492    );
7493    anyhow::ensure!(
7494        state.deployment_id == keys.deployment_id(),
7495        "Execution payload deployment ID does not match this database"
7496    );
7497    anyhow::ensure!(
7498        state.active_key_id.as_slice() == keys.active_key_id(),
7499        "Configured active payload key does not match the database active key"
7500    );
7501    Ok(())
7502}
7503
7504async fn lock_execution_payload_operation(
7505    transaction: &mut Transaction<'_, Postgres>,
7506) -> anyhow::Result<()> {
7507    let lock = PgAdvisoryLock::new("nautilus:blockchain:execution-payload");
7508    let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
7509        unreachable!("string advisory locks use the 64-bit key space");
7510    };
7511    sqlx::query("SELECT pg_advisory_xact_lock($1)")
7512        .bind(*lock_key)
7513        .execute(&mut **transaction)
7514        .await
7515        .context("failed to acquire execution payload operation fence")?;
7516    Ok(())
7517}
7518
7519async fn reserve_execution_payload_seal(
7520    transaction: &mut Transaction<'_, Postgres>,
7521    key_id: &[u8; 32],
7522) -> anyhow::Result<()> {
7523    let seals = sqlx::query_scalar::<_, i64>(
7524        "INSERT INTO execution_payload_key_state (key_id, seals) VALUES ($1, 1) \
7525         ON CONFLICT (key_id) DO UPDATE SET seals = execution_payload_key_state.seals + 1 \
7526         WHERE execution_payload_key_state.seals < $2 \
7527         RETURNING seals",
7528    )
7529    .bind(key_id.as_slice())
7530    .bind(EXECUTION_PAYLOAD_MAX_SEALS - 1)
7531    .fetch_optional(&mut **transaction)
7532    .await
7533    .context("failed to reserve execution payload nonce use")?;
7534    anyhow::ensure!(
7535        seals.is_some(),
7536        "Execution payload key reached its seal limit; rotate the active key before continuing"
7537    );
7538    Ok(())
7539}
7540
7541async fn validate_execution_payload_key_inventory(
7542    transaction: &mut Transaction<'_, Postgres>,
7543    keys: &PayloadKeySet,
7544) -> anyhow::Result<()> {
7545    let envelopes = sqlx::query_scalar::<_, Vec<u8>>(
7546        "SELECT DISTINCT substring(sealed_transaction FROM 1 FOR 33) \
7547         FROM execution_transaction_hash \
7548         WHERE sealed_transaction IS NOT NULL",
7549    )
7550    .fetch_all(&mut **transaction)
7551    .await
7552    .context("failed to inspect execution payload key inventory")?;
7553
7554    for header in envelopes {
7555        anyhow::ensure!(
7556            header.len() == 33,
7557            "Stored execution payload has a truncated envelope header"
7558        );
7559        let mut envelope = header;
7560        envelope.extend_from_slice(&[0; 12 + 16]);
7561        let key_id = envelope_key_id(&envelope)?;
7562        anyhow::ensure!(
7563            keys.contains_key(&key_id),
7564            "Stored execution payload requires unavailable key {}",
7565            alloy::hex::encode(key_id)
7566        );
7567    }
7568    Ok(())
7569}
7570
7571async fn load_execution_intent(
7572    transaction: &mut Transaction<'_, Postgres>,
7573    intent_id: i64,
7574) -> anyhow::Result<ExecutionIntentRow> {
7575    sqlx::query_as::<_, ExecutionIntentRow>(
7576        "
7577        SELECT
7578            id, schema_version, chain_id, wallet_address, nonce, purpose, status,
7579            client_order_id, trader_id, strategy_id, account_id, instrument_id,
7580            pool_address, transaction_to, transaction_input, transaction_value,
7581            amount_in, created_block, acknowledgement_emitted, fill_emitted,
7582            terminal_emitted, active
7583        FROM execution_intent
7584        WHERE id = $1
7585        FOR SHARE
7586        ",
7587    )
7588    .bind(intent_id)
7589    .fetch_optional(&mut **transaction)
7590    .await
7591    .context("failed to load execution intent for payload authentication")?
7592    .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))
7593}
7594
7595fn execution_transition_allowed(current: &str, next: TransactionStatus) -> bool {
7596    if current == next.as_str() {
7597        return true;
7598    }
7599
7600    match current {
7601        "prepared" => matches!(
7602            next,
7603            TransactionStatus::Signed | TransactionStatus::Recoverable
7604        ),
7605        "signed" | "broadcast" => matches!(
7606            next,
7607            TransactionStatus::Broadcast
7608                | TransactionStatus::Included
7609                | TransactionStatus::Finalized
7610                | TransactionStatus::Reverted
7611                | TransactionStatus::Replaced
7612                | TransactionStatus::Dropped
7613                | TransactionStatus::Reorged
7614        ),
7615        "included" => matches!(
7616            next,
7617            TransactionStatus::Finalized
7618                | TransactionStatus::Reverted
7619                | TransactionStatus::Reorged
7620                | TransactionStatus::Replaced
7621                | TransactionStatus::Dropped
7622        ),
7623        "replaced" | "dropped" | "reorged" => matches!(
7624            next,
7625            TransactionStatus::Included
7626                | TransactionStatus::Finalized
7627                | TransactionStatus::Reverted
7628                | TransactionStatus::Replaced
7629                | TransactionStatus::Dropped
7630                | TransactionStatus::Reorged
7631        ),
7632        "finalized" | "reverted" | "recoverable" => false,
7633        _ => false,
7634    }
7635}
7636
7637#[cfg(test)]
7638pub(crate) mod tests {
7639    use std::time::Duration;
7640
7641    use anyhow::Context;
7642    use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
7643
7644    use super::{BlockchainCacheDatabase, ExecutionVerifiedHeader};
7645    use crate::rpc::verification::VERIFICATION_SCHEMA_VERSION;
7646
7647    pub(crate) struct ExecutionVerificationResume {
7648        pub next_canonical_nonce: u64,
7649        pub revision: u64,
7650        pub finalized_headers: Vec<ExecutionVerifiedHeader>,
7651    }
7652
7653    pub(crate) async fn connect_test_database(
7654        pg_options: PgConnectOptions,
7655    ) -> anyhow::Result<BlockchainCacheDatabase> {
7656        let pool = PgPoolOptions::new()
7657            .max_connections(32)
7658            .min_connections(1)
7659            .acquire_timeout(Duration::from_secs(3))
7660            .connect_with(pg_options)
7661            .await?;
7662        Ok(BlockchainCacheDatabase { pool })
7663    }
7664
7665    impl BlockchainCacheDatabase {
7666        /// Loads the complete durable finalized-header ledger for tests
7667        pub(crate) async fn load_execution_verification_resume(
7668            &self,
7669            chain_id: u32,
7670            wallet_address: &str,
7671            manifest_version: &str,
7672            manifest_digest: &str,
7673        ) -> anyhow::Result<Option<ExecutionVerificationResume>> {
7674            let installed = sqlx::query_scalar::<_, i16>(
7675                "SELECT version FROM execution_schema_version \
7676                 WHERE component = 'evm_execution_verification'",
7677            )
7678            .fetch_optional(&self.pool)
7679            .await
7680            .context("failed to inspect execution verification schema")?;
7681            let Some(installed) = installed else {
7682                return Ok(None);
7683            };
7684            anyhow::ensure!(
7685                installed <= VERIFICATION_SCHEMA_VERSION,
7686                "Unsupported execution verification schema version {installed}"
7687            );
7688            let chain_id = i32::try_from(chain_id)
7689                .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
7690            let current = sqlx::query_as::<_, (String, String, i64, i64)>(
7691                "
7692                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
7693                FROM execution_verification_nonce
7694                WHERE chain_id = $1 AND wallet_address = $2
7695                ",
7696            )
7697            .bind(chain_id)
7698            .bind(wallet_address)
7699            .fetch_optional(&self.pool)
7700            .await
7701            .context("failed to load execution verification nonce position")?;
7702            let Some((stored_version, stored_digest, nonce, revision)) = current else {
7703                return Ok(None);
7704            };
7705            anyhow::ensure!(
7706                stored_version == manifest_version && stored_digest == manifest_digest,
7707                "Execution verification manifest identity changed"
7708            );
7709            let rows = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
7710                "
7711                SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
7712                FROM execution_verified_finalized_header
7713                WHERE chain_id = $1 AND wallet_address = $2
7714                ORDER BY number
7715                ",
7716            )
7717            .bind(chain_id)
7718            .bind(wallet_address)
7719            .fetch_all(&self.pool)
7720            .await
7721            .context("failed to load verified finalized header ledger")?;
7722            let finalized_headers = rows
7723                .into_iter()
7724                .map(|(number, hash, parent_hash, timestamp, base_fee, digest)| {
7725                    anyhow::ensure!(
7726                        digest == manifest_digest,
7727                        "Finalized header manifest identity changed"
7728                    );
7729                    Ok(ExecutionVerifiedHeader {
7730                        number: u64::try_from(number)
7731                            .context("Finalized header number is negative")?,
7732                        hash,
7733                        parent_hash,
7734                        timestamp: u64::try_from(timestamp)
7735                            .context("Finalized header timestamp is negative")?,
7736                        base_fee_per_gas: base_fee
7737                            .map(|value| {
7738                                value.parse::<u128>().map_err(|_| {
7739                                    anyhow::anyhow!("Finalized header base fee is invalid")
7740                                })
7741                            })
7742                            .transpose()?,
7743                    })
7744                })
7745                .collect::<anyhow::Result<Vec<_>>>()?;
7746            anyhow::ensure!(
7747                !finalized_headers.is_empty()
7748                    && finalized_headers.windows(2).all(|headers| {
7749                        headers[1].number == headers[0].number.saturating_add(1)
7750                            && headers[1].parent_hash == headers[0].hash
7751                    }),
7752                "Verified finalized header ledger is not continuous"
7753            );
7754            Ok(Some(ExecutionVerificationResume {
7755                next_canonical_nonce: u64::try_from(nonce)
7756                    .context("Canonical nonce is negative")?,
7757                revision: u64::try_from(revision)
7758                    .context("Canonical nonce revision is negative")?,
7759                finalized_headers,
7760            }))
7761        }
7762    }
7763
7764    #[tokio::test]
7765    async fn connect_returns_err_for_unreachable_database() {
7766        // `connect` backs the Python `load_pool_snapshot` binding, so a connection
7767        // failure must surface as `Err` rather than panicking across the API boundary.
7768        let options = PgConnectOptions::new()
7769            .host("127.0.0.1")
7770            .port(1)
7771            .username("nautilus")
7772            .password("pass")
7773            .database("nautilus");
7774
7775        let result = BlockchainCacheDatabase::connect(options).await;
7776
7777        assert!(result.is_err());
7778    }
7779}