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};
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                            chain.clone(),
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) => {
3267                transform_row_to_dex_pool_data(&row, chain.clone(), dex.clone(), instrument_id)
3268                    .map_err(|e| anyhow::anyhow!("Steam pool event transform error: {e}"))
3269            }
3270            Err(e) => Err(anyhow::anyhow!("Stream pool events database error: {e}")),
3271        });
3272
3273        Box::pin(stream)
3274    }
3275
3276    /// Persists an execution transaction record to the `execution_transaction` table.
3277    ///
3278    /// Records are written before broadcast so a signed transaction is never forgotten;
3279    /// the unique `(chain_id, transaction_hash)` constraint makes an exact re-insertion
3280    /// idempotent. Signer nonce ownership and order IDs are unique before broadcast. Order
3281    /// submission records carry the client order ID; operator transactions (wrap, approve)
3282    /// store `NULL`.
3283    ///
3284    /// # Errors
3285    ///
3286    /// Returns an error if the database operation fails.
3287    #[expect(
3288        clippy::too_many_arguments,
3289        reason = "the parameters mirror the persisted execution transaction fields"
3290    )]
3291    pub async fn add_execution_transaction(
3292        &self,
3293        chain_id: u32,
3294        wallet_address: &str,
3295        nonce: u64,
3296        transaction_hash: &str,
3297        purpose: &str,
3298        status: &str,
3299        client_order_id: Option<&str>,
3300    ) -> anyhow::Result<()> {
3301        let result = sqlx::query(
3302            "
3303            INSERT INTO execution_transaction (
3304                chain_id,
3305                wallet_address,
3306                nonce,
3307                transaction_hash,
3308                purpose,
3309                status,
3310                client_order_id
3311            )
3312            VALUES ($1, $2, $3, $4, $5, $6, $7)
3313            ON CONFLICT (chain_id, transaction_hash)
3314            DO UPDATE SET transaction_hash = EXCLUDED.transaction_hash
3315            WHERE execution_transaction.wallet_address = EXCLUDED.wallet_address
3316              AND execution_transaction.nonce = EXCLUDED.nonce
3317              AND execution_transaction.purpose = EXCLUDED.purpose
3318              AND execution_transaction.status = EXCLUDED.status
3319              AND execution_transaction.client_order_id IS NOT DISTINCT FROM EXCLUDED.client_order_id
3320        ",
3321        )
3322        .bind(chain_id as i32)
3323        .bind(wallet_address)
3324        .bind(nonce as i64)
3325        .bind(transaction_hash)
3326        .bind(purpose)
3327        .bind(status)
3328        .bind(client_order_id)
3329        .execute(&self.pool)
3330        .await
3331        .map_err(|e| anyhow::anyhow!("Failed to insert into execution_transaction table: {e}"))?;
3332
3333        anyhow::ensure!(
3334            result.rows_affected() == 1,
3335            "Execution transaction {transaction_hash} conflicts with its persisted record"
3336        );
3337        Ok(())
3338    }
3339
3340    /// Installs execution schema version 2 without changing existing transaction rows.
3341    ///
3342    /// The migration locks the legacy transaction table, refuses unresolved version 1 rows,
3343    /// installs the versioned intent and hash-history tables, and fences older writers before
3344    /// releasing the lock. This prevents a mixed-version process from bypassing the new signer
3345    /// ownership constraints.
3346    ///
3347    /// # Errors
3348    ///
3349    /// Returns an error if the database operation fails.
3350    pub async fn ensure_execution_transaction_schema(&self) -> anyhow::Result<()> {
3351        let mut transaction = self
3352            .pool
3353            .begin()
3354            .await
3355            .map_err(|e| anyhow::anyhow!("Failed to start execution schema migration: {e}"))?;
3356
3357        sqlx::query("LOCK TABLE execution_transaction IN ACCESS EXCLUSIVE MODE")
3358            .execute(&mut *transaction)
3359            .await
3360            .map_err(|e| anyhow::anyhow!("Failed to lock legacy execution transactions: {e}"))?;
3361
3362        for statement in [
3363            "
3364            ALTER TABLE execution_transaction
3365            ADD COLUMN IF NOT EXISTS client_order_id TEXT
3366            ",
3367            "
3368            ALTER TABLE execution_transaction
3369            ADD COLUMN IF NOT EXISTS wallet_address TEXT
3370            ",
3371            "
3372            ALTER TABLE execution_transaction
3373            ALTER COLUMN wallet_address DROP NOT NULL
3374            ",
3375            "
3376            CREATE TABLE IF NOT EXISTS execution_schema_version (
3377                component TEXT PRIMARY KEY,
3378                version SMALLINT NOT NULL CHECK (version > 0)
3379            )
3380            ",
3381            "
3382            CREATE TABLE IF NOT EXISTS execution_intent (
3383                id BIGSERIAL PRIMARY KEY,
3384                schema_version SMALLINT NOT NULL,
3385                chain_id INTEGER NOT NULL REFERENCES chain(chain_id) ON DELETE RESTRICT,
3386                wallet_address TEXT NOT NULL,
3387                nonce BIGINT,
3388                purpose TEXT NOT NULL,
3389                status TEXT NOT NULL,
3390                client_order_id TEXT,
3391                trader_id TEXT,
3392                strategy_id TEXT,
3393                account_id TEXT,
3394                instrument_id TEXT,
3395                pool_address TEXT,
3396                transaction_to TEXT NOT NULL,
3397                transaction_input TEXT NOT NULL,
3398                transaction_value TEXT NOT NULL,
3399                amount_in TEXT,
3400                created_block BIGINT NOT NULL,
3401                acknowledgement_emitted BOOLEAN NOT NULL DEFAULT FALSE,
3402                fill_emitted BOOLEAN NOT NULL DEFAULT FALSE,
3403                terminal_emitted BOOLEAN NOT NULL DEFAULT FALSE,
3404                active BOOLEAN NOT NULL DEFAULT TRUE,
3405                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3406                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3407                CHECK (schema_version = 2),
3408                CHECK (nonce IS NULL OR nonce >= 0),
3409                CHECK (created_block >= 0),
3410                CHECK (status IN (
3411                    'prepared', 'signed', 'broadcast', 'included', 'finalized',
3412                    'reverted', 'replaced', 'dropped', 'reorged', 'recoverable'
3413                )),
3414                CONSTRAINT execution_intent_active_check CHECK (
3415                    NOT active
3416                    OR status NOT IN ('finalized', 'reverted', 'recoverable')
3417                    OR (
3418                        status IN ('finalized', 'reverted')
3419                        AND NOT (fill_emitted OR terminal_emitted)
3420                    )
3421                ),
3422                CHECK (NOT (fill_emitted AND terminal_emitted)),
3423                CHECK (
3424                    purpose <> 'swap'
3425                    OR (
3426                        client_order_id IS NOT NULL
3427                        AND trader_id IS NOT NULL
3428                        AND strategy_id IS NOT NULL
3429                        AND account_id IS NOT NULL
3430                        AND instrument_id IS NOT NULL
3431                        AND pool_address IS NOT NULL
3432                        AND amount_in IS NOT NULL
3433                    )
3434                ),
3435                UNIQUE (id, chain_id)
3436            )
3437            ",
3438            "
3439            CREATE UNIQUE INDEX IF NOT EXISTS execution_intent_active_signer_key
3440            ON execution_intent (chain_id, wallet_address)
3441            WHERE active
3442            ",
3443            "
3444            CREATE UNIQUE INDEX IF NOT EXISTS execution_intent_active_nonce_key
3445            ON execution_intent (chain_id, wallet_address, nonce)
3446            WHERE active AND nonce IS NOT NULL
3447            ",
3448            "
3449            CREATE UNIQUE INDEX IF NOT EXISTS execution_intent_client_order_key
3450            ON execution_intent (chain_id, wallet_address, client_order_id)
3451            WHERE client_order_id IS NOT NULL
3452            ",
3453            "
3454            ALTER TABLE execution_intent
3455            DROP CONSTRAINT IF EXISTS execution_intent_active_check
3456            ",
3457            "
3458            ALTER TABLE execution_intent
3459            ADD CONSTRAINT execution_intent_active_check CHECK (
3460                NOT active
3461                OR status NOT IN ('finalized', 'reverted', 'recoverable')
3462                OR (
3463                    status IN ('finalized', 'reverted')
3464                    AND NOT (fill_emitted OR terminal_emitted)
3465                )
3466            )
3467            ",
3468            "
3469            CREATE TABLE IF NOT EXISTS execution_transaction_hash (
3470                id BIGSERIAL PRIMARY KEY,
3471                intent_id BIGINT NOT NULL,
3472                chain_id INTEGER NOT NULL,
3473                transaction_hash TEXT NOT NULL,
3474                payload_expected BOOLEAN NOT NULL DEFAULT TRUE,
3475                raw_transaction BYTEA,
3476                sealed_transaction BYTEA,
3477                status TEXT NOT NULL,
3478                block_number BIGINT,
3479                block_hash TEXT,
3480                receipt_success BOOLEAN,
3481                gas_used BIGINT,
3482                effective_gas_price TEXT,
3483                current BOOLEAN NOT NULL DEFAULT TRUE,
3484                created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3485                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3486                FOREIGN KEY (intent_id, chain_id)
3487                    REFERENCES execution_intent(id, chain_id) ON DELETE RESTRICT,
3488                CHECK (block_number IS NULL OR block_number >= 0),
3489                CHECK (gas_used IS NULL OR gas_used >= 0),
3490                CONSTRAINT execution_transaction_raw_size_check
3491                    CHECK (raw_transaction IS NULL OR octet_length(raw_transaction) <= 131072),
3492                CONSTRAINT execution_transaction_sealed_size_check
3493                    CHECK (sealed_transaction IS NULL OR octet_length(sealed_transaction) <= 131133),
3494                CHECK (status IN (
3495                    'signed', 'broadcast', 'included', 'finalized', 'reverted',
3496                    'replaced', 'dropped', 'reorged'
3497                )),
3498                UNIQUE (chain_id, transaction_hash),
3499                UNIQUE (intent_id, transaction_hash)
3500            )
3501            ",
3502            "
3503            ALTER TABLE execution_transaction_hash
3504            ADD COLUMN IF NOT EXISTS payload_expected BOOLEAN
3505            ",
3506            "
3507            UPDATE execution_transaction_hash
3508            SET payload_expected = raw_transaction IS NOT NULL
3509            WHERE payload_expected IS NULL
3510            ",
3511            "
3512            ALTER TABLE execution_transaction_hash
3513            ALTER COLUMN payload_expected SET DEFAULT TRUE
3514            ",
3515            "
3516            ALTER TABLE execution_transaction_hash
3517            ALTER COLUMN payload_expected SET NOT NULL
3518            ",
3519            "
3520            ALTER TABLE execution_transaction_hash
3521            ADD COLUMN IF NOT EXISTS sealed_transaction BYTEA
3522            ",
3523            "
3524            ALTER TABLE execution_transaction_hash
3525            DROP CONSTRAINT IF EXISTS execution_transaction_raw_size_check
3526            ",
3527            "
3528            ALTER TABLE execution_transaction_hash
3529            ADD CONSTRAINT execution_transaction_raw_size_check
3530            CHECK (raw_transaction IS NULL OR octet_length(raw_transaction) <= 131072)
3531            ",
3532            "
3533            ALTER TABLE execution_transaction_hash
3534            DROP CONSTRAINT IF EXISTS execution_transaction_sealed_size_check
3535            ",
3536            "
3537            ALTER TABLE execution_transaction_hash
3538            ADD CONSTRAINT execution_transaction_sealed_size_check
3539            CHECK (sealed_transaction IS NULL OR octet_length(sealed_transaction) <= 131133)
3540            ",
3541            "
3542            CREATE TABLE IF NOT EXISTS execution_payload_state (
3543                component TEXT PRIMARY KEY CHECK (component = 'signed_transactions'),
3544                deployment_id TEXT NOT NULL CHECK (deployment_id <> ''),
3545                protocol_version SMALLINT NOT NULL CHECK (protocol_version = 1),
3546                operation TEXT NOT NULL CHECK (operation IN ('migrate', 'ready', 'rewrap', 'rollback')),
3547                active_key_id BYTEA NOT NULL CHECK (octet_length(active_key_id) = 32),
3548                progress_id BIGINT NOT NULL DEFAULT 0 CHECK (progress_id >= 0),
3549                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
3550            )
3551            ",
3552            "
3553            CREATE TABLE IF NOT EXISTS execution_payload_key_state (
3554                key_id BYTEA PRIMARY KEY CHECK (octet_length(key_id) = 32),
3555                seals BIGINT NOT NULL DEFAULT 0 CHECK (seals >= 0 AND seals < 4294967296)
3556            )
3557            ",
3558            "
3559            CREATE UNIQUE INDEX IF NOT EXISTS execution_transaction_hash_current_key
3560            ON execution_transaction_hash (intent_id)
3561            WHERE current
3562            ",
3563            "
3564            CREATE TABLE IF NOT EXISTS execution_transaction_transition (
3565                id BIGSERIAL PRIMARY KEY,
3566                intent_id BIGINT NOT NULL REFERENCES execution_intent(id) ON DELETE RESTRICT,
3567                transaction_hash_id BIGINT REFERENCES execution_transaction_hash(id) ON DELETE RESTRICT,
3568                transition_key TEXT NOT NULL,
3569                from_status TEXT,
3570                to_status TEXT NOT NULL,
3571                block_number BIGINT,
3572                block_hash TEXT,
3573                observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3574                CHECK (block_number IS NULL OR block_number >= 0),
3575                CHECK (to_status IN (
3576                    'prepared', 'signed', 'broadcast', 'included', 'finalized',
3577                    'reverted', 'replaced', 'dropped', 'reorged', 'recoverable'
3578                )),
3579                UNIQUE (intent_id, transition_key)
3580            )
3581            ",
3582        ] {
3583            sqlx::query(statement)
3584                .execute(&mut *transaction)
3585                .await
3586                .map_err(|e| {
3587                    anyhow::anyhow!("Failed to migrate execution_transaction table: {e}")
3588                })?;
3589        }
3590
3591        let installed_version = sqlx::query_scalar::<_, i16>(
3592            "SELECT version FROM execution_schema_version WHERE component = 'evm_execution'",
3593        )
3594        .fetch_optional(&mut *transaction)
3595        .await
3596        .map_err(|e| anyhow::anyhow!("Failed to read execution schema version: {e}"))?;
3597        if let Some(installed_version) = installed_version
3598            && installed_version > EXECUTION_SCHEMA_VERSION
3599        {
3600            anyhow::bail!(
3601                "Execution schema version {} is newer than supported version {EXECUTION_SCHEMA_VERSION}",
3602                installed_version
3603            );
3604        }
3605
3606        let unresolved_legacy = sqlx::query_scalar::<_, i64>(
3607            "
3608            SELECT COUNT(*)
3609            FROM execution_transaction
3610            WHERE status IN ('pending', 'included', 'reverted')
3611            ",
3612        )
3613        .fetch_one(&mut *transaction)
3614        .await
3615        .map_err(|e| anyhow::anyhow!("Failed to inspect legacy execution transactions: {e}"))?;
3616        anyhow::ensure!(
3617            unresolved_legacy == 0,
3618            "Cannot safely migrate {unresolved_legacy} unresolved execution schema version 1 transaction(s); resolve them with the prior version before enabling version {EXECUTION_SCHEMA_VERSION}"
3619        );
3620
3621        for statement in [
3622            "
3623            CREATE OR REPLACE FUNCTION execution_transaction_v2_fence()
3624            RETURNS TRIGGER AS $$
3625            BEGIN
3626                RAISE EXCEPTION 'Legacy execution writer refused after schema version 2 activation';
3627            END;
3628            $$ LANGUAGE plpgsql
3629            ",
3630            "DROP TRIGGER IF EXISTS execution_transaction_v2_fence ON execution_transaction",
3631            "
3632            CREATE TRIGGER execution_transaction_v2_fence
3633            BEFORE INSERT OR UPDATE OR DELETE ON execution_transaction
3634            FOR EACH STATEMENT EXECUTE FUNCTION execution_transaction_v2_fence()
3635            ",
3636            "
3637            CREATE OR REPLACE FUNCTION execution_transition_append_only()
3638            RETURNS TRIGGER AS $$
3639            BEGIN
3640                RAISE EXCEPTION 'Execution transitions are append-only';
3641            END;
3642            $$ LANGUAGE plpgsql
3643            ",
3644            "DROP TRIGGER IF EXISTS execution_transition_append_only ON execution_transaction_transition",
3645            "
3646            CREATE TRIGGER execution_transition_append_only
3647            BEFORE UPDATE OR DELETE ON execution_transaction_transition
3648            FOR EACH STATEMENT EXECUTE FUNCTION execution_transition_append_only()
3649            ",
3650            "
3651            INSERT INTO execution_schema_version (component, version)
3652            VALUES ('evm_execution', 2)
3653            ON CONFLICT (component) DO UPDATE SET version = EXCLUDED.version
3654            WHERE execution_schema_version.version <= EXCLUDED.version
3655            ",
3656        ] {
3657            sqlx::query(statement)
3658                .execute(&mut *transaction)
3659                .await
3660                .map_err(|e| {
3661                    anyhow::anyhow!("Failed to activate execution schema version 2: {e}")
3662                })?;
3663        }
3664
3665        transaction
3666            .commit()
3667            .await
3668            .map_err(|e| anyhow::anyhow!("Failed to commit execution schema migration: {e}"))?;
3669
3670        Ok(())
3671    }
3672
3673    /// Loads the durable finalized-header and nonce position when verification is active.
3674    pub(crate) async fn load_execution_verification_position(
3675        &self,
3676        chain_id: u32,
3677        wallet_address: &str,
3678        manifest_version: &str,
3679        manifest_digest: &str,
3680    ) -> anyhow::Result<Option<ExecutionVerificationPosition>> {
3681        let installed = sqlx::query_scalar::<_, i16>(
3682            "SELECT version FROM execution_schema_version \
3683             WHERE component = 'evm_execution_verification'",
3684        )
3685        .fetch_optional(&self.pool)
3686        .await
3687        .context("failed to inspect execution verification schema")?;
3688        let Some(installed) = installed else {
3689            return Ok(None);
3690        };
3691        anyhow::ensure!(
3692            installed <= VERIFICATION_SCHEMA_VERSION,
3693            "Unsupported execution verification schema version {installed}"
3694        );
3695        let chain_id =
3696            i32::try_from(chain_id).context("Verification chain ID exceeds PostgreSQL INTEGER")?;
3697        let current = sqlx::query_as::<_, (String, String, i64, i64)>(
3698            "
3699            SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
3700            FROM execution_verification_nonce
3701            WHERE chain_id = $1 AND wallet_address = $2
3702            ",
3703        )
3704        .bind(chain_id)
3705        .bind(wallet_address)
3706        .fetch_optional(&self.pool)
3707        .await
3708        .context("failed to load execution verification nonce position")?;
3709        let Some((stored_version, stored_digest, nonce, revision)) = current else {
3710            return Ok(None);
3711        };
3712        anyhow::ensure!(
3713            stored_version == manifest_version && stored_digest == manifest_digest,
3714            "Execution verification manifest identity changed"
3715        );
3716        let row = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
3717            "
3718            SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
3719            FROM execution_verified_finalized_header
3720            WHERE chain_id = $1 AND wallet_address = $2
3721            ORDER BY number DESC
3722            LIMIT 1
3723            ",
3724        )
3725        .bind(chain_id)
3726        .bind(wallet_address)
3727        .fetch_optional(&self.pool)
3728        .await
3729        .context("failed to load verified finalized header tip")?
3730        .ok_or_else(|| anyhow::anyhow!("Verified finalized header ledger is empty"))?;
3731        let (number, hash, parent_hash, timestamp, base_fee, digest) = row;
3732        anyhow::ensure!(
3733            digest == manifest_digest,
3734            "Finalized header manifest identity changed"
3735        );
3736        Ok(Some(ExecutionVerificationPosition {
3737            next_canonical_nonce: u64::try_from(nonce).context("Canonical nonce is negative")?,
3738            revision: u64::try_from(revision).context("Canonical nonce revision is negative")?,
3739            finalized_tip: ExecutionVerifiedHeader {
3740                number: u64::try_from(number).context("Finalized header number is negative")?,
3741                hash,
3742                parent_hash,
3743                timestamp: u64::try_from(timestamp)
3744                    .context("Finalized header timestamp is negative")?,
3745                base_fee_per_gas: base_fee
3746                    .map(|value| {
3747                        value
3748                            .parse::<u128>()
3749                            .map_err(|_| anyhow::anyhow!("Finalized header base fee is invalid"))
3750                    })
3751                    .transpose()?,
3752            },
3753        }))
3754    }
3755
3756    pub(crate) async fn load_execution_verified_header(
3757        &self,
3758        chain_id: u32,
3759        wallet_address: &str,
3760        number: u64,
3761        manifest_digest: &str,
3762    ) -> anyhow::Result<Option<ExecutionVerifiedHeader>> {
3763        let chain_id =
3764            i32::try_from(chain_id).context("Verification chain ID exceeds PostgreSQL INTEGER")?;
3765        let number =
3766            i64::try_from(number).context("Finalized header number exceeds PostgreSQL BIGINT")?;
3767        let row = sqlx::query_as::<_, (String, String, i64, Option<String>, String)>(
3768            "
3769            SELECT hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
3770            FROM execution_verified_finalized_header
3771            WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
3772            ",
3773        )
3774        .bind(chain_id)
3775        .bind(wallet_address)
3776        .bind(number)
3777        .fetch_optional(&self.pool)
3778        .await
3779        .context("failed to load verified finalized header")?;
3780        let Some((hash, parent_hash, timestamp, base_fee, digest)) = row else {
3781            return Ok(None);
3782        };
3783        anyhow::ensure!(
3784            digest == manifest_digest,
3785            "Finalized header manifest identity changed"
3786        );
3787        Ok(Some(ExecutionVerifiedHeader {
3788            number: u64::try_from(number).context("Finalized header number is negative")?,
3789            hash,
3790            parent_hash,
3791            timestamp: u64::try_from(timestamp)
3792                .context("Finalized header timestamp is negative")?,
3793            base_fee_per_gas: base_fee
3794                .map(|value| {
3795                    value
3796                        .parse::<u128>()
3797                        .map_err(|_| anyhow::anyhow!("Finalized header base fee is invalid"))
3798                })
3799                .transpose()?,
3800        }))
3801    }
3802
3803    /// Loads the retained execution state that a first verification bootstrap must classify.
3804    pub(crate) async fn load_execution_verification_migration_snapshot(
3805        &self,
3806        chain_id: u32,
3807        wallet_address: &str,
3808    ) -> anyhow::Result<ExecutionVerificationMigrationSnapshot> {
3809        let chain_id =
3810            i32::try_from(chain_id).context("Verification chain ID exceeds PostgreSQL INTEGER")?;
3811        let intents = sqlx::query_as::<_, ExecutionIntentRow>(
3812            "
3813            SELECT
3814                id, schema_version, chain_id, wallet_address, nonce, purpose, status,
3815                client_order_id, trader_id, strategy_id, account_id, instrument_id,
3816                pool_address, transaction_to, transaction_input, transaction_value,
3817                amount_in, created_block, acknowledgement_emitted, fill_emitted,
3818                terminal_emitted, active
3819            FROM execution_intent
3820            WHERE chain_id = $1 AND wallet_address = $2
3821            ORDER BY id
3822            ",
3823        )
3824        .bind(chain_id)
3825        .bind(wallet_address)
3826        .fetch_all(&self.pool)
3827        .await
3828        .context("failed to load retained execution intents for verification migration")?;
3829        let hashes = sqlx::query_as::<_, ExecutionTransactionHashRow>(
3830            "
3831            SELECT
3832                hash.id, hash.intent_id, hash.chain_id, hash.transaction_hash,
3833                hash.payload_expected, hash.raw_transaction, hash.sealed_transaction,
3834                hash.status, hash.block_number, hash.block_hash, hash.receipt_success,
3835                hash.gas_used, hash.effective_gas_price, hash.current
3836            FROM execution_transaction_hash AS hash
3837            JOIN execution_intent AS intent ON intent.id = hash.intent_id
3838            WHERE intent.chain_id = $1 AND intent.wallet_address = $2
3839            ORDER BY hash.id
3840            ",
3841        )
3842        .bind(chain_id)
3843        .bind(wallet_address)
3844        .fetch_all(&self.pool)
3845        .await
3846        .context("failed to load retained execution hashes for verification migration")?;
3847        Ok(ExecutionVerificationMigrationSnapshot { intents, hashes })
3848    }
3849
3850    /// Installs or validates the independent verification ledger for an execution signer.
3851    ///
3852    /// A database with retained execution intents but no verification ledger requires an archive-
3853    /// verified migration which classifies every retained intent. An empty database initializes
3854    /// its canonical nonce from a verified finalized-height transaction count and its header
3855    /// ledger from the trusted checkpoint.
3856    ///
3857    /// # Errors
3858    ///
3859    /// Returns an error if the schema, migration snapshot, historical reconstruction, or retained
3860    /// evidence is inconsistent, or persistence fails.
3861    pub(crate) async fn ensure_execution_verification_schema(
3862        &self,
3863        bootstrap: &ExecutionVerificationBootstrap<'_>,
3864    ) -> anyhow::Result<()> {
3865        let first_header = bootstrap
3866            .finalized_headers
3867            .first()
3868            .ok_or_else(|| anyhow::anyhow!("Verified finalized header ledger is empty"))?;
3869        anyhow::ensure!(
3870            bootstrap.finalized_headers.windows(2).all(|headers| {
3871                headers[1].number == headers[0].number.saturating_add(1)
3872                    && headers[1].parent_hash == headers[0].hash
3873            }),
3874            "Verified finalized headers are not one continuous parent-linked chain"
3875        );
3876        anyhow::ensure!(
3877            bootstrap.provider_ids.len() == 3
3878                && bootstrap.operator_ids.len() == 3
3879                && bootstrap.failure_domain_ids.len() >= 3
3880                && !bootstrap.decisions.is_empty(),
3881            "Connect verification evidence is incomplete"
3882        );
3883        let chain_id = i32::try_from(bootstrap.chain_id)
3884            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
3885        let checkpoint_number = i64::try_from(bootstrap.checkpoint_number)
3886            .context("Verification checkpoint exceeds PostgreSQL BIGINT")?;
3887        let checkpoint_timestamp = i64::try_from(bootstrap.checkpoint_timestamp)
3888            .context("Verification checkpoint timestamp exceeds PostgreSQL BIGINT")?;
3889        let next_canonical_nonce = i64::try_from(bootstrap.next_canonical_nonce)
3890            .context("Canonical nonce exceeds PostgreSQL BIGINT")?;
3891        let observed_canonical_nonce = i64::try_from(bootstrap.observed_canonical_nonce)
3892            .context("Observed canonical nonce exceeds PostgreSQL BIGINT")?;
3893        let base_fee = bootstrap
3894            .checkpoint_base_fee_per_gas
3895            .map(|value| value.to_string());
3896        let mut transaction = self.pool.begin().await.map_err(|e| {
3897            anyhow::anyhow!("Failed to start execution verification migration: {e}")
3898        })?;
3899
3900        for statement in [
3901            "
3902            CREATE TABLE IF NOT EXISTS execution_verification_nonce (
3903                chain_id INTEGER NOT NULL REFERENCES chain(chain_id) ON DELETE RESTRICT,
3904                wallet_address TEXT NOT NULL,
3905                manifest_version TEXT NOT NULL CHECK (manifest_version <> ''),
3906                manifest_digest TEXT NOT NULL CHECK (manifest_digest <> ''),
3907                next_canonical_nonce BIGINT NOT NULL CHECK (next_canonical_nonce >= 0),
3908                revision BIGINT NOT NULL DEFAULT 0 CHECK (revision >= 0),
3909                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3910                PRIMARY KEY (chain_id, wallet_address)
3911            )
3912            ",
3913            "
3914            CREATE TABLE IF NOT EXISTS execution_verified_finalized_header (
3915                chain_id INTEGER NOT NULL,
3916                wallet_address TEXT NOT NULL,
3917                number BIGINT NOT NULL CHECK (number >= 0),
3918                hash TEXT NOT NULL CHECK (hash <> ''),
3919                parent_hash TEXT NOT NULL CHECK (parent_hash <> ''),
3920                timestamp BIGINT NOT NULL CHECK (timestamp >= 0),
3921                base_fee_per_gas TEXT,
3922                manifest_digest TEXT NOT NULL CHECK (manifest_digest <> ''),
3923                observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3924                PRIMARY KEY (chain_id, wallet_address, number),
3925                UNIQUE (chain_id, wallet_address, hash),
3926                FOREIGN KEY (chain_id, wallet_address)
3927                    REFERENCES execution_verification_nonce(chain_id, wallet_address)
3928                    ON DELETE RESTRICT
3929            )
3930            ",
3931            "
3932            CREATE TABLE IF NOT EXISTS execution_verification_decision (
3933                id BIGSERIAL PRIMARY KEY,
3934                intent_id BIGINT REFERENCES execution_intent(id) ON DELETE RESTRICT,
3935                nonce BIGINT CHECK (nonce IS NULL OR nonce >= 0),
3936                decision_class TEXT NOT NULL CHECK (decision_class <> ''),
3937                read_class TEXT NOT NULL CHECK (read_class <> ''),
3938                height_start BIGINT CHECK (height_start IS NULL OR height_start >= 0),
3939                height_end BIGINT CHECK (height_end IS NULL OR height_end >= 0),
3940                manifest_version TEXT NOT NULL CHECK (manifest_version <> ''),
3941                manifest_digest TEXT NOT NULL CHECK (manifest_digest <> ''),
3942                provider_ids TEXT[] NOT NULL,
3943                operator_ids TEXT[] NOT NULL,
3944                failure_domain_ids TEXT[] NOT NULL,
3945                response_class TEXT NOT NULL CHECK (response_class <> ''),
3946                normalized_value_digest TEXT,
3947                safe_value JSONB,
3948                nonce_revision BIGINT CHECK (nonce_revision IS NULL OR nonce_revision >= 0),
3949                outcome TEXT NOT NULL CHECK (outcome IN (
3950                    'verified', 'disagreement', 'unavailable', 'retryable', 'locally_invalid'
3951                )),
3952                transition_key TEXT,
3953                observed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3954                CHECK (cardinality(provider_ids) = 3),
3955                CHECK (cardinality(operator_ids) = 3),
3956                CHECK (cardinality(failure_domain_ids) >= 3),
3957                CHECK (safe_value IS NULL OR pg_column_size(safe_value) <= 16384),
3958                UNIQUE (intent_id, transition_key)
3959            )
3960            ",
3961            "
3962            CREATE TABLE IF NOT EXISTS execution_replacement_scan (
3963                intent_id BIGINT PRIMARY KEY REFERENCES execution_intent(id) ON DELETE RESTRICT,
3964                chain_id INTEGER NOT NULL,
3965                wallet_address TEXT NOT NULL,
3966                nonce BIGINT NOT NULL CHECK (nonce >= 0),
3967                finalized_cursor_number BIGINT NOT NULL CHECK (finalized_cursor_number >= 0),
3968                finalized_cursor_hash TEXT NOT NULL CHECK (finalized_cursor_hash <> ''),
3969                manifest_digest TEXT NOT NULL CHECK (manifest_digest <> ''),
3970                updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
3971                UNIQUE (chain_id, wallet_address, nonce)
3972            )
3973            ",
3974        ] {
3975            sqlx::query(statement)
3976                .execute(&mut *transaction)
3977                .await
3978                .map_err(|e| anyhow::anyhow!("Failed to install verification schema: {e}"))?;
3979        }
3980
3981        sqlx::query(
3982            "LOCK TABLE execution_intent, execution_transaction_hash, \
3983             execution_verification_nonce, execution_verified_finalized_header, \
3984             execution_verification_decision, execution_replacement_scan \
3985             IN ACCESS EXCLUSIVE MODE",
3986        )
3987        .execute(&mut *transaction)
3988        .await
3989        .map_err(|e| anyhow::anyhow!("Failed to lock execution verification state: {e}"))?;
3990
3991        let installed_version = sqlx::query_scalar::<_, i16>(
3992            "SELECT version FROM execution_schema_version \
3993             WHERE component = 'evm_execution_verification'",
3994        )
3995        .fetch_optional(&mut *transaction)
3996        .await
3997        .map_err(|e| anyhow::anyhow!("Failed to read verification schema version: {e}"))?;
3998        if let Some(installed_version) = installed_version {
3999            anyhow::ensure!(
4000                installed_version <= VERIFICATION_SCHEMA_VERSION,
4001                "Execution verification schema version {installed_version} is newer than supported version {VERIFICATION_SCHEMA_VERSION}"
4002            );
4003        }
4004
4005        let current = sqlx::query_as::<_, (String, String, i64, i64)>(
4006            "
4007            SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
4008            FROM execution_verification_nonce
4009            WHERE chain_id = $1 AND wallet_address = $2
4010            FOR UPDATE
4011            ",
4012        )
4013        .bind(chain_id)
4014        .bind(bootstrap.wallet_address)
4015        .fetch_optional(&mut *transaction)
4016        .await
4017        .map_err(|e| anyhow::anyhow!("Failed to read canonical nonce ledger: {e}"))?;
4018        let initialized = current.is_some();
4019
4020        let revision = if let Some((manifest_version, manifest_digest, stored_nonce, revision)) =
4021            current
4022        {
4023            anyhow::ensure!(
4024                bootstrap.migration.is_none(),
4025                "Verification migration was supplied for an initialized signer"
4026            );
4027            anyhow::ensure!(
4028                manifest_version == bootstrap.manifest_version
4029                    && manifest_digest == bootstrap.manifest_digest,
4030                "Execution verification manifest identity changed"
4031            );
4032            anyhow::ensure!(
4033                stored_nonce == next_canonical_nonce,
4034                "Canonical nonce ledger changed during verification bootstrap"
4035            );
4036
4037            if observed_canonical_nonce != stored_nonce {
4038                let expected_observed_nonce = stored_nonce
4039                    .checked_add(1)
4040                    .ok_or_else(|| anyhow::anyhow!("Canonical nonce overflow"))?;
4041                anyhow::ensure!(
4042                    observed_canonical_nonce == expected_observed_nonce,
4043                    "Verified finalized transaction count is outside the owned recovery range"
4044                );
4045                let recovery = sqlx::query_as::<_, (Option<i64>, String, i64)>(
4046                    "
4047                    SELECT
4048                        intent.nonce,
4049                        intent.status,
4050                        COUNT(hash.id) FILTER (
4051                            WHERE hash.current
4052                              AND hash.payload_expected
4053                              AND ((hash.raw_transaction IS NOT NULL)::INTEGER
4054                                   + (hash.sealed_transaction IS NOT NULL)::INTEGER) = 1
4055                              AND hash.status IN (
4056                                  'broadcast', 'included', 'replaced', 'dropped', 'reorged'
4057                              )
4058                        )
4059                    FROM execution_intent AS intent
4060                    LEFT JOIN execution_transaction_hash AS hash
4061                        ON hash.intent_id = intent.id
4062                    WHERE intent.chain_id = $1
4063                      AND intent.wallet_address = $2
4064                      AND intent.active
4065                    GROUP BY intent.id, intent.nonce, intent.status
4066                    ",
4067                )
4068                .bind(chain_id)
4069                .bind(bootstrap.wallet_address)
4070                .fetch_optional(&mut *transaction)
4071                .await
4072                .map_err(|e| anyhow::anyhow!("Failed to validate nonce recovery ownership: {e}"))?;
4073                let Some((intent_nonce, intent_status, payload_count)) = recovery else {
4074                    anyhow::bail!(
4075                        "Verified finalized transaction count advanced without an active owned intent"
4076                    );
4077                };
4078                anyhow::ensure!(
4079                    intent_nonce == Some(stored_nonce)
4080                        && matches!(
4081                            intent_status.as_str(),
4082                            "broadcast" | "included" | "replaced" | "dropped" | "reorged"
4083                        )
4084                        && payload_count == 1,
4085                    "Verified finalized transaction count advanced without one recoverable retained payload at the durable nonce"
4086                );
4087            }
4088            revision
4089        } else {
4090            anyhow::ensure!(
4091                next_canonical_nonce == observed_canonical_nonce,
4092                "Initial canonical nonce conflicts with the verified finalized transaction count"
4093            );
4094            let locked_intents = sqlx::query_as::<_, ExecutionIntentRow>(
4095                "
4096                SELECT
4097                    id, schema_version, chain_id, wallet_address, nonce, purpose, status,
4098                    client_order_id, trader_id, strategy_id, account_id, instrument_id,
4099                    pool_address, transaction_to, transaction_input, transaction_value,
4100                    amount_in, created_block, acknowledgement_emitted, fill_emitted,
4101                    terminal_emitted, active
4102                FROM execution_intent
4103                WHERE chain_id = $1 AND wallet_address = $2
4104                ORDER BY id
4105                ",
4106            )
4107            .bind(chain_id)
4108            .bind(bootstrap.wallet_address)
4109            .fetch_all(&mut *transaction)
4110            .await
4111            .map_err(|e| anyhow::anyhow!("Failed to lock retained execution intents: {e}"))?;
4112            let locked_hashes = sqlx::query_as::<_, ExecutionTransactionHashRow>(
4113                "
4114                SELECT
4115                    hash.id, hash.intent_id, hash.chain_id, hash.transaction_hash,
4116                    hash.payload_expected, hash.raw_transaction, hash.sealed_transaction,
4117                    hash.status, hash.block_number, hash.block_hash, hash.receipt_success,
4118                    hash.gas_used, hash.effective_gas_price, hash.current
4119                FROM execution_transaction_hash AS hash
4120                JOIN execution_intent AS intent ON intent.id = hash.intent_id
4121                WHERE intent.chain_id = $1 AND intent.wallet_address = $2
4122                ORDER BY hash.id
4123                ",
4124            )
4125            .bind(chain_id)
4126            .bind(bootstrap.wallet_address)
4127            .fetch_all(&mut *transaction)
4128            .await
4129            .map_err(|e| anyhow::anyhow!("Failed to lock retained execution hashes: {e}"))?;
4130            if locked_intents.is_empty() {
4131                anyhow::ensure!(
4132                    bootstrap.migration.is_none(),
4133                    "Verification migration was supplied for empty execution history"
4134                );
4135            } else {
4136                let migration = bootstrap.migration.ok_or_else(|| {
4137                    anyhow::anyhow!(
4138                        "Retained execution history requires archive verification before migration"
4139                    )
4140                })?;
4141                anyhow::ensure!(
4142                    migration.snapshot.intents == locked_intents
4143                        && migration.snapshot.hashes == locked_hashes,
4144                    "Retained execution history changed during verification migration"
4145                );
4146                let intent_ids = locked_intents
4147                    .iter()
4148                    .map(|intent| intent.id)
4149                    .collect::<BTreeSet<_>>();
4150                let record_ids = migration
4151                    .records
4152                    .iter()
4153                    .map(|record| record.intent_id)
4154                    .collect::<BTreeSet<_>>();
4155                anyhow::ensure!(
4156                    intent_ids == record_ids && record_ids.len() == migration.records.len(),
4157                    "Verification migration does not classify every retained intent exactly once"
4158                );
4159            }
4160            sqlx::query(
4161                "
4162                INSERT INTO execution_verification_nonce (
4163                    chain_id, wallet_address, manifest_version, manifest_digest,
4164                    next_canonical_nonce
4165                )
4166                VALUES ($1, $2, $3, $4, $5)
4167                ",
4168            )
4169            .bind(chain_id)
4170            .bind(bootstrap.wallet_address)
4171            .bind(bootstrap.manifest_version)
4172            .bind(bootstrap.manifest_digest)
4173            .bind(next_canonical_nonce)
4174            .execute(&mut *transaction)
4175            .await
4176            .map_err(|e| anyhow::anyhow!("Failed to initialize canonical nonce ledger: {e}"))?;
4177
4178            if let Some(migration) = bootstrap.migration {
4179                for record in &migration.records {
4180                    anyhow::ensure!(
4181                        !record.decisions.is_empty(),
4182                        "Verification migration evidence is empty for intent {}",
4183                        record.intent_id
4184                    );
4185
4186                    if record.recover_prepared {
4187                        anyhow::ensure!(
4188                            record.terminal_status.is_none()
4189                                && record.transaction_hash.is_none()
4190                                && record.nonce.is_none(),
4191                            "Prepared recovery migration record is inconsistent"
4192                        );
4193                        let result = sqlx::query(
4194                            "
4195                            UPDATE execution_intent
4196                            SET status = 'recoverable', active = FALSE, updated_at = NOW()
4197                            WHERE id = $1 AND active AND status = 'prepared' AND nonce IS NULL
4198                              AND NOT EXISTS (
4199                                  SELECT 1 FROM execution_transaction_hash WHERE intent_id = $1
4200                              )
4201                            ",
4202                        )
4203                        .bind(record.intent_id)
4204                        .execute(&mut *transaction)
4205                        .await
4206                        .map_err(|e| {
4207                            anyhow::anyhow!("Failed to recover migrated prepared intent: {e}")
4208                        })?;
4209                        anyhow::ensure!(
4210                            result.rows_affected() == 1,
4211                            "Prepared intent {} changed during verification migration",
4212                            record.intent_id
4213                        );
4214                    }
4215
4216                    if let Some(status) = record.terminal_status {
4217                        anyhow::ensure!(
4218                            matches!(
4219                                status,
4220                                TransactionStatus::Finalized | TransactionStatus::Reverted
4221                            ),
4222                            "Migration terminal status is not finalized or reverted"
4223                        );
4224                        let transaction_hash =
4225                            record.transaction_hash.as_deref().ok_or_else(|| {
4226                                anyhow::anyhow!("Terminal migration record has no transaction hash")
4227                            })?;
4228                        let block_number = i64::try_from(record.block_number.ok_or_else(|| {
4229                            anyhow::anyhow!("Terminal migration record has no block number")
4230                        })?)
4231                        .context("Migration block exceeds PostgreSQL BIGINT")?;
4232                        let block_hash = record.block_hash.as_deref().ok_or_else(|| {
4233                            anyhow::anyhow!("Terminal migration record has no block hash")
4234                        })?;
4235                        let gas_used = i64::try_from(record.gas_used.ok_or_else(|| {
4236                            anyhow::anyhow!("Terminal migration record has no gas usage")
4237                        })?)
4238                        .context("Migration gas usage exceeds PostgreSQL BIGINT")?;
4239                        let receipt_success = record.receipt_success.ok_or_else(|| {
4240                            anyhow::anyhow!("Terminal migration record has no receipt status")
4241                        })?;
4242                        anyhow::ensure!(
4243                            receipt_success == (status == TransactionStatus::Finalized),
4244                            "Migration receipt status conflicts with terminal status"
4245                        );
4246                        let current_status = sqlx::query_scalar::<_, String>(
4247                            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
4248                        )
4249                        .bind(record.intent_id)
4250                        .fetch_one(&mut *transaction)
4251                        .await
4252                        .map_err(|e| anyhow::anyhow!("Failed to lock migrated intent: {e}"))?;
4253                        let hash_result = sqlx::query(
4254                            "
4255                            UPDATE execution_transaction_hash
4256                            SET status = $3, block_number = $4, block_hash = $5,
4257                                receipt_success = $6, gas_used = $7,
4258                                effective_gas_price = $8, updated_at = NOW()
4259                            WHERE intent_id = $1 AND transaction_hash = $2 AND current
4260                              AND payload_expected
4261                            ",
4262                        )
4263                        .bind(record.intent_id)
4264                        .bind(transaction_hash)
4265                        .bind(status.as_str())
4266                        .bind(block_number)
4267                        .bind(block_hash)
4268                        .bind(receipt_success)
4269                        .bind(gas_used)
4270                        .bind(record.effective_gas_price.as_deref())
4271                        .execute(&mut *transaction)
4272                        .await
4273                        .map_err(|e| anyhow::anyhow!("Failed to reconstruct migrated hash: {e}"))?;
4274                        anyhow::ensure!(
4275                            hash_result.rows_affected() == 1,
4276                            "Migrated terminal transaction hash was not found"
4277                        );
4278                        sqlx::query(
4279                            "UPDATE execution_intent SET status = $2, updated_at = NOW() WHERE id = $1",
4280                        )
4281                        .bind(record.intent_id)
4282                        .bind(status.as_str())
4283                        .execute(&mut *transaction)
4284                        .await
4285                        .map_err(|e| anyhow::anyhow!("Failed to reconstruct migrated intent: {e}"))?;
4286                        if current_status != status.as_str() {
4287                            sqlx::query(
4288                                "
4289                                INSERT INTO execution_transaction_transition (
4290                                    intent_id, transaction_hash_id, transition_key,
4291                                    from_status, to_status, block_number, block_hash
4292                                )
4293                                SELECT $1, id, $3, $4, $5, $6, $7
4294                                FROM execution_transaction_hash
4295                                WHERE intent_id = $1 AND transaction_hash = $2
4296                                ",
4297                            )
4298                            .bind(record.intent_id)
4299                            .bind(transaction_hash)
4300                            .bind(format!("migration:{}:{transaction_hash}", status.as_str()))
4301                            .bind(current_status)
4302                            .bind(status.as_str())
4303                            .bind(block_number)
4304                            .bind(block_hash)
4305                            .execute(&mut *transaction)
4306                            .await
4307                            .map_err(|e| {
4308                                anyhow::anyhow!(
4309                                    "Failed to record migrated terminal transition: {e}"
4310                                )
4311                            })?;
4312                        }
4313                    }
4314
4315                    let nonce = record
4316                        .nonce
4317                        .map(i64::try_from)
4318                        .transpose()
4319                        .context("Migration nonce exceeds PostgreSQL BIGINT")?;
4320
4321                    for (index, decision) in record.decisions.iter().enumerate() {
4322                        let height_start = decision
4323                            .height_start
4324                            .map(i64::try_from)
4325                            .transpose()
4326                            .context("Migration evidence height exceeds PostgreSQL BIGINT")?;
4327                        let height_end = decision
4328                            .height_end
4329                            .map(i64::try_from)
4330                            .transpose()
4331                            .context("Migration evidence height exceeds PostgreSQL BIGINT")?;
4332                        sqlx::query(
4333                            "
4334                            INSERT INTO execution_verification_decision (
4335                                intent_id, nonce, decision_class, read_class,
4336                                height_start, height_end, manifest_version, manifest_digest,
4337                                provider_ids, operator_ids, failure_domain_ids,
4338                                response_class, normalized_value_digest, nonce_revision,
4339                                outcome, transition_key
4340                            )
4341                            VALUES (
4342                                $1, $2, 'migration', $3, $4, $5, $6, $7, $8, $9, $10,
4343                                'all_valid', $11, 0, 'verified', $12
4344                            )
4345                            ",
4346                        )
4347                        .bind(record.intent_id)
4348                        .bind(nonce)
4349                        .bind(decision.read_class)
4350                        .bind(height_start)
4351                        .bind(height_end)
4352                        .bind(bootstrap.manifest_version)
4353                        .bind(bootstrap.manifest_digest)
4354                        .bind(bootstrap.provider_ids)
4355                        .bind(bootstrap.operator_ids)
4356                        .bind(bootstrap.failure_domain_ids)
4357                        .bind(&decision.normalized_value_digest)
4358                        .bind(format!("migration:{}:{index}", record.intent_id))
4359                        .execute(&mut *transaction)
4360                        .await
4361                        .map_err(|e| {
4362                            anyhow::anyhow!("Failed to persist migration evidence: {e}")
4363                        })?;
4364                    }
4365                }
4366            }
4367            0
4368        };
4369
4370        sqlx::query(
4371            "
4372            INSERT INTO execution_verified_finalized_header (
4373                chain_id, wallet_address, number, hash, parent_hash, timestamp,
4374                base_fee_per_gas, manifest_digest
4375            )
4376            VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
4377            ON CONFLICT (chain_id, wallet_address, number) DO NOTHING
4378            ",
4379        )
4380        .bind(chain_id)
4381        .bind(bootstrap.wallet_address)
4382        .bind(checkpoint_number)
4383        .bind(bootstrap.checkpoint_hash)
4384        .bind(bootstrap.checkpoint_parent_hash)
4385        .bind(checkpoint_timestamp)
4386        .bind(base_fee)
4387        .bind(bootstrap.manifest_digest)
4388        .execute(&mut *transaction)
4389        .await
4390        .map_err(|e| anyhow::anyhow!("Failed to initialize finalized header ledger: {e}"))?;
4391        let stored_checkpoint = sqlx::query_as::<_, (String, String, i64, Option<String>)>(
4392            "
4393            SELECT hash, parent_hash, timestamp, base_fee_per_gas
4394            FROM execution_verified_finalized_header
4395            WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
4396            ",
4397        )
4398        .bind(chain_id)
4399        .bind(bootstrap.wallet_address)
4400        .bind(checkpoint_number)
4401        .fetch_one(&mut *transaction)
4402        .await
4403        .map_err(|e| anyhow::anyhow!("Failed to validate finalized checkpoint ledger: {e}"))?;
4404        anyhow::ensure!(
4405            stored_checkpoint
4406                == (
4407                    bootstrap.checkpoint_hash.to_string(),
4408                    bootstrap.checkpoint_parent_hash.to_string(),
4409                    checkpoint_timestamp,
4410                    bootstrap
4411                        .checkpoint_base_fee_per_gas
4412                        .map(|value| value.to_string()),
4413                ),
4414            "Finalized checkpoint ledger conflicts with the trusted chain anchor"
4415        );
4416
4417        if initialized {
4418            let stored_tip =
4419                sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
4420                    "
4421                SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
4422                FROM execution_verified_finalized_header
4423                WHERE chain_id = $1 AND wallet_address = $2
4424                ORDER BY number DESC
4425                LIMIT 1
4426                ",
4427                )
4428                .bind(chain_id)
4429                .bind(bootstrap.wallet_address)
4430                .fetch_one(&mut *transaction)
4431                .await
4432                .map_err(|e| anyhow::anyhow!("Failed to lock finalized header tip: {e}"))?;
4433            anyhow::ensure!(
4434                stored_tip
4435                    == (
4436                        i64::try_from(first_header.number)
4437                            .context("Verified finalized height exceeds PostgreSQL BIGINT")?,
4438                        first_header.hash.clone(),
4439                        first_header.parent_hash.clone(),
4440                        i64::try_from(first_header.timestamp)
4441                            .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?,
4442                        first_header.base_fee_per_gas.map(|value| value.to_string()),
4443                        bootstrap.manifest_digest.to_string(),
4444                    ),
4445                "Verified finalized header extension does not start at the durable tip"
4446            );
4447        } else {
4448            anyhow::ensure!(
4449                first_header.number == bootstrap.checkpoint_number
4450                    && first_header.hash == bootstrap.checkpoint_hash
4451                    && first_header.parent_hash == bootstrap.checkpoint_parent_hash
4452                    && first_header.timestamp == bootstrap.checkpoint_timestamp
4453                    && first_header.base_fee_per_gas == bootstrap.checkpoint_base_fee_per_gas,
4454                "Verified finalized headers do not start at the trusted checkpoint"
4455            );
4456        }
4457
4458        for header in bootstrap.finalized_headers.iter().skip(1) {
4459            let number = i64::try_from(header.number)
4460                .context("Verified finalized height exceeds PostgreSQL BIGINT")?;
4461            let timestamp = i64::try_from(header.timestamp)
4462                .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?;
4463            let base_fee = header.base_fee_per_gas.map(|value| value.to_string());
4464            sqlx::query(
4465                "
4466                INSERT INTO execution_verified_finalized_header (
4467                    chain_id, wallet_address, number, hash, parent_hash, timestamp,
4468                    base_fee_per_gas, manifest_digest
4469                )
4470                VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
4471                ON CONFLICT (chain_id, wallet_address, number) DO NOTHING
4472                ",
4473            )
4474            .bind(chain_id)
4475            .bind(bootstrap.wallet_address)
4476            .bind(number)
4477            .bind(&header.hash)
4478            .bind(&header.parent_hash)
4479            .bind(timestamp)
4480            .bind(&base_fee)
4481            .bind(bootstrap.manifest_digest)
4482            .execute(&mut *transaction)
4483            .await
4484            .map_err(|e| anyhow::anyhow!("Failed to extend finalized header ledger: {e}"))?;
4485            let stored = sqlx::query_as::<_, (String, String, i64, Option<String>, String)>(
4486                "
4487                SELECT hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
4488                FROM execution_verified_finalized_header
4489                WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
4490                ",
4491            )
4492            .bind(chain_id)
4493            .bind(bootstrap.wallet_address)
4494            .bind(number)
4495            .fetch_one(&mut *transaction)
4496            .await
4497            .map_err(|e| anyhow::anyhow!("Failed to validate finalized header ledger: {e}"))?;
4498            anyhow::ensure!(
4499                stored
4500                    == (
4501                        header.hash.clone(),
4502                        header.parent_hash.clone(),
4503                        timestamp,
4504                        base_fee,
4505                        bootstrap.manifest_digest.to_string(),
4506                    ),
4507                "Finalized header ledger conflicts at height {}",
4508                header.number
4509            );
4510        }
4511
4512        let finalized_height = bootstrap
4513            .finalized_headers
4514            .last()
4515            .expect("verified finalized headers are nonempty")
4516            .number;
4517
4518        for (index, decision) in bootstrap.decisions.iter().enumerate() {
4519            let height_start = decision
4520                .height_start
4521                .map(i64::try_from)
4522                .transpose()
4523                .context("Connect verification height exceeds PostgreSQL BIGINT")?;
4524            let height_end = decision
4525                .height_end
4526                .map(i64::try_from)
4527                .transpose()
4528                .context("Connect verification height exceeds PostgreSQL BIGINT")?;
4529            let transition_key = format!("connect:{finalized_height}:{revision}:{index}");
4530            sqlx::query(
4531                "
4532                INSERT INTO execution_verification_decision (
4533                    intent_id, nonce, decision_class, read_class, height_start, height_end,
4534                    manifest_version, manifest_digest, provider_ids, operator_ids,
4535                    failure_domain_ids, response_class, normalized_value_digest,
4536                    nonce_revision, outcome, transition_key
4537                )
4538                VALUES (
4539                    NULL, NULL, 'connect', $1, $2, $3, $4, $5, $6, $7, $8,
4540                    'all_valid', $9, $10, 'verified', $11
4541                )
4542                ",
4543            )
4544            .bind(decision.read_class)
4545            .bind(height_start)
4546            .bind(height_end)
4547            .bind(bootstrap.manifest_version)
4548            .bind(bootstrap.manifest_digest)
4549            .bind(bootstrap.provider_ids)
4550            .bind(bootstrap.operator_ids)
4551            .bind(bootstrap.failure_domain_ids)
4552            .bind(&decision.normalized_value_digest)
4553            .bind(revision)
4554            .bind(transition_key)
4555            .execute(&mut *transaction)
4556            .await
4557            .map_err(|e| anyhow::anyhow!("Failed to persist connect verification: {e}"))?;
4558        }
4559
4560        for statement in [
4561            "
4562            CREATE OR REPLACE FUNCTION execution_verification_append_only()
4563            RETURNS TRIGGER AS $$
4564            BEGIN
4565                RAISE EXCEPTION 'Execution verification evidence is append-only';
4566            END;
4567            $$ LANGUAGE plpgsql
4568            ",
4569            "DROP TRIGGER IF EXISTS execution_verification_decision_append_only \
4570             ON execution_verification_decision",
4571            "
4572            CREATE TRIGGER execution_verification_decision_append_only
4573            BEFORE UPDATE OR DELETE ON execution_verification_decision
4574            FOR EACH STATEMENT EXECUTE FUNCTION execution_verification_append_only()
4575            ",
4576            "DROP TRIGGER IF EXISTS execution_finalized_header_append_only \
4577             ON execution_verified_finalized_header",
4578            "
4579            CREATE TRIGGER execution_finalized_header_append_only
4580            BEFORE UPDATE OR DELETE ON execution_verified_finalized_header
4581            FOR EACH STATEMENT EXECUTE FUNCTION execution_verification_append_only()
4582            ",
4583        ] {
4584            sqlx::query(statement)
4585                .execute(&mut *transaction)
4586                .await
4587                .map_err(|e| anyhow::anyhow!("Failed to activate verification schema: {e}"))?;
4588        }
4589        sqlx::query(
4590            "
4591            INSERT INTO execution_schema_version (component, version)
4592            VALUES ('evm_execution_verification', $1)
4593            ON CONFLICT (component) DO UPDATE SET version = EXCLUDED.version
4594            WHERE execution_schema_version.version <= EXCLUDED.version
4595            ",
4596        )
4597        .bind(VERIFICATION_SCHEMA_VERSION)
4598        .execute(&mut *transaction)
4599        .await
4600        .map_err(|e| anyhow::anyhow!("Failed to activate verification schema: {e}"))?;
4601
4602        transaction
4603            .commit()
4604            .await
4605            .map_err(|e| anyhow::anyhow!("Failed to commit verification migration: {e}"))?;
4606        Ok(())
4607    }
4608
4609    /// Activates or resumes protected signed-transaction storage for this database.
4610    pub(crate) async fn ensure_execution_payload_storage(
4611        &self,
4612        keys: &PayloadKeySet,
4613    ) -> anyhow::Result<()> {
4614        let marker = self.execution_payload_marker().await?;
4615        match marker {
4616            None => self.activate_execution_payload_storage(keys).await?,
4617            Some(version) => anyhow::ensure!(
4618                version == EXECUTION_PAYLOAD_PROTOCOL_VERSION,
4619                "Execution payload protection version {version} is newer than supported version {EXECUTION_PAYLOAD_PROTOCOL_VERSION}"
4620            ),
4621        }
4622
4623        loop {
4624            let state = self.execution_payload_state().await?.ok_or_else(|| {
4625                anyhow::anyhow!("Execution payload marker exists without durable state")
4626            })?;
4627            validate_execution_payload_state(&state, keys)?;
4628            match state.operation.as_str() {
4629                "migrate" => {
4630                    if self
4631                        .migrate_execution_payload_batch(keys, EXECUTION_PAYLOAD_BATCH_SIZE)
4632                        .await?
4633                    {
4634                        break;
4635                    }
4636                }
4637                "ready" => break,
4638                operation => anyhow::bail!(
4639                    "Execution payload storage is in {operation} maintenance; complete or roll back that operation before connecting"
4640                ),
4641            }
4642        }
4643
4644        self.validate_execution_payload_ready(keys).await
4645    }
4646
4647    /// Requires ready protected storage and authenticates every persisted signed payload.
4648    pub(crate) async fn require_execution_payload_storage(
4649        &self,
4650        keys: &PayloadKeySet,
4651        policy: PayloadPolicy,
4652        batch_size: i64,
4653    ) -> anyhow::Result<ExecutionPayloadLease> {
4654        self.require_execution_payload_storage_ready(keys).await?;
4655        let (check, transaction) = self
4656            .inspect_execution_payload_storage(Some(keys), Some(policy), batch_size)
4657            .await?;
4658        anyhow::ensure!(
4659            check.protected,
4660            "Postgres execution requires protected payload storage"
4661        );
4662        Ok(ExecutionPayloadLease {
4663            _transaction: transaction,
4664        })
4665    }
4666
4667    /// Requires protected storage to be ready before execution schema initialization.
4668    pub(crate) async fn require_execution_payload_storage_ready(
4669        &self,
4670        keys: &PayloadKeySet,
4671    ) -> anyhow::Result<()> {
4672        anyhow::ensure!(
4673            self.execution_payload_marker().await?.is_some(),
4674            "Postgres execution requires protected payload storage"
4675        );
4676        self.validate_execution_payload_ready(keys).await
4677    }
4678
4679    /// Acquires a shared state lease for signing, payload access, acknowledgment, or broadcast.
4680    pub(crate) async fn acquire_execution_payload_lease(
4681        &self,
4682        keys: &PayloadKeySet,
4683    ) -> anyhow::Result<ExecutionPayloadLease> {
4684        let mut transaction = self
4685            .pool
4686            .begin()
4687            .await
4688            .context("failed to start execution payload action lease")?;
4689        let marker = sqlx::query_scalar::<_, i16>(
4690            "SELECT version FROM execution_schema_version WHERE component = $1",
4691        )
4692        .bind(EXECUTION_PAYLOAD_COMPONENT)
4693        .fetch_optional(&mut *transaction)
4694        .await
4695        .context("failed to read execution payload marker")?;
4696
4697        let version = marker.ok_or_else(|| {
4698            anyhow::anyhow!("Postgres execution requires protected payload storage")
4699        })?;
4700        anyhow::ensure!(
4701            version == EXECUTION_PAYLOAD_PROTOCOL_VERSION,
4702            "Unsupported execution payload protection version {version}"
4703        );
4704        let row = sqlx::query(
4705            "SELECT deployment_id, protocol_version, operation, active_key_id \
4706             FROM execution_payload_state WHERE component = 'signed_transactions' \
4707             FOR SHARE",
4708        )
4709        .fetch_optional(&mut *transaction)
4710        .await
4711        .context("failed to lock execution payload state")?
4712        .ok_or_else(|| anyhow::anyhow!("Execution payload marker exists without state"))?;
4713        let state = execution_payload_state_from_row(&row)?;
4714        validate_execution_payload_state(&state, keys)?;
4715        anyhow::ensure!(
4716            state.operation == "ready",
4717            "Execution payload storage is in {} maintenance",
4718            state.operation
4719        );
4720
4721        Ok(ExecutionPayloadLease {
4722            _transaction: transaction,
4723        })
4724    }
4725
4726    /// Reserves one nonce use under the active payload key.
4727    pub(crate) async fn reserve_execution_payload_seal(
4728        &self,
4729        keys: &PayloadKeySet,
4730    ) -> anyhow::Result<()> {
4731        let mut transaction = self
4732            .pool
4733            .begin()
4734            .await
4735            .context("failed to start payload seal reservation")?;
4736        let row = sqlx::query(
4737            "SELECT deployment_id, protocol_version, operation, active_key_id \
4738             FROM execution_payload_state WHERE component = 'signed_transactions' FOR SHARE",
4739        )
4740        .fetch_optional(&mut *transaction)
4741        .await
4742        .context("failed to lock execution payload state")?
4743        .ok_or_else(|| anyhow::anyhow!("Execution payload protection is not active"))?;
4744        let state = execution_payload_state_from_row(&row)?;
4745        validate_execution_payload_state(&state, keys)?;
4746        anyhow::ensure!(
4747            state.operation == "ready",
4748            "Execution payload storage is in {} maintenance",
4749            state.operation
4750        );
4751        reserve_execution_payload_seal(&mut transaction, keys.active_key_id()).await?;
4752        transaction
4753            .commit()
4754            .await
4755            .context("failed to commit payload seal reservation")?;
4756        Ok(())
4757    }
4758
4759    async fn execution_payload_marker(&self) -> anyhow::Result<Option<i16>> {
4760        sqlx::query_scalar::<_, i16>(
4761            "SELECT version FROM execution_schema_version WHERE component = $1",
4762        )
4763        .bind(EXECUTION_PAYLOAD_COMPONENT)
4764        .fetch_optional(&self.pool)
4765        .await
4766        .context("failed to read execution payload marker")
4767    }
4768
4769    async fn execution_payload_state(&self) -> anyhow::Result<Option<ExecutionPayloadState>> {
4770        let row = sqlx::query(
4771            "SELECT deployment_id, protocol_version, operation, active_key_id \
4772             FROM execution_payload_state WHERE component = 'signed_transactions'",
4773        )
4774        .fetch_optional(&self.pool)
4775        .await
4776        .context("failed to read execution payload state")?;
4777        row.as_ref()
4778            .map(execution_payload_state_from_row)
4779            .transpose()
4780    }
4781
4782    async fn activate_execution_payload_storage(&self, keys: &PayloadKeySet) -> anyhow::Result<()> {
4783        let mut transaction = self
4784            .pool
4785            .begin()
4786            .await
4787            .context("failed to start execution payload activation")?;
4788        lock_execution_payload_operation(&mut transaction).await?;
4789        sqlx::query("LOCK TABLE execution_transaction_hash IN ACCESS EXCLUSIVE MODE")
4790            .execute(&mut *transaction)
4791            .await
4792            .context("failed to fence execution payload writers")?;
4793        let marker = sqlx::query_scalar::<_, i16>(
4794            "SELECT version FROM execution_schema_version WHERE component = $1",
4795        )
4796        .bind(EXECUTION_PAYLOAD_COMPONENT)
4797        .fetch_optional(&mut *transaction)
4798        .await
4799        .context("failed to recheck execution payload marker")?;
4800        if marker.is_some() {
4801            transaction
4802                .rollback()
4803                .await
4804                .context("failed to release concurrent payload activation")?;
4805            return Ok(());
4806        }
4807        let sealed_rows = sqlx::query_scalar::<_, i64>(
4808            "SELECT COUNT(*) FROM execution_transaction_hash WHERE sealed_transaction IS NOT NULL",
4809        )
4810        .fetch_one(&mut *transaction)
4811        .await
4812        .context("failed to inspect pre-activation envelopes")?;
4813        anyhow::ensure!(
4814            sealed_rows == 0,
4815            "Unprotected execution payload storage contains {sealed_rows} sealed row(s)"
4816        );
4817
4818        for statement in [
4819            "
4820            CREATE OR REPLACE FUNCTION execution_transaction_payload_fence()
4821            RETURNS TRIGGER AS $$
4822            DECLARE payload_operation TEXT;
4823            BEGIN
4824                IF NEW.raw_transaction IS NOT NULL
4825                   AND (TG_OP = 'INSERT'
4826                        OR OLD.raw_transaction IS NULL
4827                        OR NEW.raw_transaction IS DISTINCT FROM OLD.raw_transaction) THEN
4828                    SELECT operation INTO payload_operation
4829                    FROM execution_payload_state
4830                    WHERE component = 'signed_transactions';
4831                    IF NOT (
4832                        TG_OP = 'UPDATE'
4833                        AND payload_operation = 'rollback'
4834                        AND OLD.payload_expected
4835                        AND OLD.raw_transaction IS NULL
4836                        AND NEW.raw_transaction IS NOT NULL
4837                        AND OLD.sealed_transaction IS NOT NULL
4838                        AND NEW.sealed_transaction = OLD.sealed_transaction
4839                    ) THEN
4840                        RAISE EXCEPTION 'Plaintext signed transaction writes are disabled';
4841                    END IF;
4842                END IF;
4843                RETURN NEW;
4844            END;
4845            $$ LANGUAGE plpgsql
4846            ",
4847            "DROP TRIGGER IF EXISTS execution_transaction_payload_fence ON execution_transaction_hash",
4848            "
4849            CREATE TRIGGER execution_transaction_payload_fence
4850            BEFORE INSERT OR UPDATE ON execution_transaction_hash
4851            FOR EACH ROW EXECUTE FUNCTION execution_transaction_payload_fence()
4852            ",
4853        ] {
4854            sqlx::query(statement)
4855                .execute(&mut *transaction)
4856                .await
4857                .context("failed to install execution payload write fence")?;
4858        }
4859        sqlx::query("INSERT INTO execution_schema_version (component, version) VALUES ($1, $2)")
4860            .bind(EXECUTION_PAYLOAD_COMPONENT)
4861            .bind(EXECUTION_PAYLOAD_PROTOCOL_VERSION)
4862            .execute(&mut *transaction)
4863            .await
4864            .context("failed to record execution payload marker")?;
4865        sqlx::query(
4866            "INSERT INTO execution_payload_state (component, deployment_id, protocol_version, operation, active_key_id) \
4867             VALUES ('signed_transactions', $1, $2, 'migrate', $3)",
4868        )
4869        .bind(keys.deployment_id())
4870        .bind(EXECUTION_PAYLOAD_PROTOCOL_VERSION)
4871        .bind(keys.active_key_id().as_slice())
4872        .execute(&mut *transaction)
4873        .await
4874        .context("failed to record execution payload migration state")?;
4875        sqlx::query(
4876            "INSERT INTO execution_payload_key_state (key_id, seals) VALUES ($1, 0) \
4877             ON CONFLICT (key_id) DO NOTHING",
4878        )
4879        .bind(keys.active_key_id().as_slice())
4880        .execute(&mut *transaction)
4881        .await
4882        .context("failed to initialize execution payload key state")?;
4883        transaction
4884            .commit()
4885            .await
4886            .context("failed to commit execution payload activation")?;
4887        Ok(())
4888    }
4889
4890    async fn migrate_execution_payload_batch(
4891        &self,
4892        keys: &PayloadKeySet,
4893        batch_size: i64,
4894    ) -> anyhow::Result<bool> {
4895        anyhow::ensure!(
4896            batch_size > 0,
4897            "Execution payload batch size must be positive"
4898        );
4899        let mut transaction = self
4900            .pool
4901            .begin()
4902            .await
4903            .context("failed to start execution payload migration batch")?;
4904        lock_execution_payload_operation(&mut transaction).await?;
4905        let state_row = sqlx::query(
4906            "SELECT deployment_id, protocol_version, operation, active_key_id \
4907             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
4908        )
4909        .fetch_optional(&mut *transaction)
4910        .await
4911        .context("failed to lock execution payload migration state")?
4912        .ok_or_else(|| anyhow::anyhow!("Execution payload migration state is missing"))?;
4913        let state = execution_payload_state_from_row(&state_row)?;
4914        validate_execution_payload_state(&state, keys)?;
4915        anyhow::ensure!(
4916            state.operation == "migrate",
4917            "Execution payload storage is {}, not migrating",
4918            state.operation
4919        );
4920
4921        let rows = sqlx::query_as::<_, ExecutionTransactionHashRow>(
4922            "
4923            SELECT
4924                id, intent_id, chain_id, transaction_hash, payload_expected,
4925                raw_transaction, sealed_transaction, status, block_number, block_hash,
4926                receipt_success, gas_used, effective_gas_price, current
4927            FROM execution_transaction_hash
4928            WHERE payload_expected AND raw_transaction IS NOT NULL
4929            ORDER BY id
4930            LIMIT $1
4931            FOR UPDATE
4932            ",
4933        )
4934        .bind(batch_size)
4935        .fetch_all(&mut *transaction)
4936        .await
4937        .context("failed to load legacy signed transaction batch")?;
4938
4939        if rows.is_empty() {
4940            self.complete_execution_payload_migration(&mut transaction, keys)
4941                .await?;
4942            transaction
4943                .commit()
4944                .await
4945                .context("failed to commit execution payload migration completion")?;
4946            return Ok(true);
4947        }
4948
4949        for hash in rows {
4950            anyhow::ensure!(
4951                hash.sealed_transaction.is_none(),
4952                "Execution transaction {} contains both plaintext and sealed payloads",
4953                hash.id
4954            );
4955            let raw_transaction = hash
4956                .raw_transaction
4957                .as_deref()
4958                .expect("migration query requires plaintext");
4959            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
4960            let context = authenticate_retained_payload(
4961                raw_transaction,
4962                &intent,
4963                &hash,
4964                keys.deployment_id(),
4965            )
4966            .with_context(|| format!("failed to authenticate execution payload {}", hash.id))?;
4967            reserve_execution_payload_seal(&mut transaction, keys.active_key_id()).await?;
4968            let envelope = keys.seal(raw_transaction, &context)?;
4969            let unsealed = keys.unseal(&envelope, &context)?;
4970            authenticate_retained_payload(&unsealed, &intent, &hash, keys.deployment_id())?;
4971            anyhow::ensure!(
4972                unsealed == raw_transaction,
4973                "Execution payload {} changed during seal round trip",
4974                hash.id
4975            );
4976            let result = sqlx::query(
4977                "UPDATE execution_transaction_hash \
4978                 SET sealed_transaction = $2, raw_transaction = NULL, updated_at = NOW() \
4979                 WHERE id = $1 AND raw_transaction = $3 AND sealed_transaction IS NULL",
4980            )
4981            .bind(hash.id)
4982            .bind(&envelope)
4983            .bind(raw_transaction)
4984            .execute(&mut *transaction)
4985            .await
4986            .context("failed to promote execution payload")?;
4987            anyhow::ensure!(
4988                result.rows_affected() == 1,
4989                "Execution payload {} changed during migration",
4990                hash.id
4991            );
4992            sqlx::query(
4993                "UPDATE execution_payload_state SET progress_id = $1, updated_at = NOW() \
4994                 WHERE component = 'signed_transactions'",
4995            )
4996            .bind(hash.id)
4997            .execute(&mut *transaction)
4998            .await
4999            .context("failed to record execution payload migration progress")?;
5000        }
5001
5002        transaction
5003            .commit()
5004            .await
5005            .context("failed to commit execution payload migration batch")?;
5006        Ok(false)
5007    }
5008
5009    async fn complete_execution_payload_migration(
5010        &self,
5011        transaction: &mut Transaction<'_, Postgres>,
5012        keys: &PayloadKeySet,
5013    ) -> anyhow::Result<()> {
5014        sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE ROW EXCLUSIVE MODE")
5015            .execute(&mut **transaction)
5016            .await
5017            .context("failed to lock execution payload migration completion")?;
5018        let invalid = sqlx::query_scalar::<_, i64>(
5019            "SELECT COUNT(*) FROM execution_transaction_hash \
5020             WHERE (payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NULL)) \
5021                OR (NOT payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NOT NULL))",
5022        )
5023        .fetch_one(&mut **transaction)
5024        .await
5025        .context("failed to verify migrated execution payload representations")?;
5026        anyhow::ensure!(
5027            invalid == 0,
5028            "Execution payload migration found {invalid} invalid representation(s)"
5029        );
5030        validate_execution_payload_key_inventory(transaction, keys).await?;
5031
5032        for statement in [
5033            "ALTER TABLE execution_transaction_hash \
5034             DROP CONSTRAINT IF EXISTS execution_transaction_payload_protected_check",
5035            "ALTER TABLE execution_transaction_hash \
5036             ADD CONSTRAINT execution_transaction_payload_protected_check CHECK ( \
5037                 (payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NOT NULL) \
5038                 OR (NOT payload_expected AND raw_transaction IS NULL AND sealed_transaction IS NULL) \
5039             )",
5040        ] {
5041            sqlx::query(statement)
5042                .execute(&mut **transaction)
5043                .await
5044                .context("failed to install protected execution payload constraint")?;
5045        }
5046        sqlx::query(
5047            "UPDATE execution_payload_state SET operation = 'ready', updated_at = NOW() \
5048             WHERE component = 'signed_transactions' AND operation = 'migrate'",
5049        )
5050        .execute(&mut **transaction)
5051        .await
5052        .context("failed to mark execution payload storage ready")?;
5053        Ok(())
5054    }
5055
5056    async fn validate_execution_payload_ready(&self, keys: &PayloadKeySet) -> anyhow::Result<()> {
5057        let mut transaction = self
5058            .pool
5059            .begin()
5060            .await
5061            .context("failed to start execution payload validation")?;
5062        let state_row = sqlx::query(
5063            "SELECT deployment_id, protocol_version, operation, active_key_id \
5064             FROM execution_payload_state WHERE component = 'signed_transactions' FOR SHARE",
5065        )
5066        .fetch_optional(&mut *transaction)
5067        .await
5068        .context("failed to lock execution payload state")?
5069        .ok_or_else(|| anyhow::anyhow!("Execution payload state is missing"))?;
5070        let state = execution_payload_state_from_row(&state_row)?;
5071        validate_execution_payload_state(&state, keys)?;
5072        anyhow::ensure!(
5073            state.operation == "ready",
5074            "Execution payload storage is not ready"
5075        );
5076        let (payload_fence, payload_constraint) =
5077            sqlx::query_as::<_, (bool, bool)>(EXECUTION_PAYLOAD_INFRASTRUCTURE_QUERY)
5078                .fetch_one(&mut *transaction)
5079                .await
5080                .context("failed to inspect execution payload protection infrastructure")?;
5081        anyhow::ensure!(
5082            payload_fence && payload_constraint,
5083            "Execution payload storage is marked ready without its write fence or constraint"
5084        );
5085        validate_execution_payload_key_inventory(&mut transaction, keys).await?;
5086        transaction
5087            .commit()
5088            .await
5089            .context("failed to complete execution payload validation")?;
5090        Ok(())
5091    }
5092
5093    pub(crate) async fn check_execution_payload_storage(
5094        &self,
5095        keys: Option<&PayloadKeySet>,
5096        policy: Option<PayloadPolicy>,
5097        batch_size: i64,
5098    ) -> anyhow::Result<ExecutionPayloadCheck> {
5099        let (check, transaction) = self
5100            .inspect_execution_payload_storage(keys, policy, batch_size)
5101            .await?;
5102        transaction
5103            .commit()
5104            .await
5105            .context("failed to complete execution payload check")?;
5106        Ok(check)
5107    }
5108
5109    async fn inspect_execution_payload_storage(
5110        &self,
5111        keys: Option<&PayloadKeySet>,
5112        policy: Option<PayloadPolicy>,
5113        batch_size: i64,
5114    ) -> anyhow::Result<(ExecutionPayloadCheck, Transaction<'static, Postgres>)> {
5115        anyhow::ensure!(
5116            batch_size > 0,
5117            "Execution payload check batch size must be positive"
5118        );
5119        let mut transaction = self
5120            .pool
5121            .begin()
5122            .await
5123            .context("failed to start execution payload check")?;
5124        sqlx::query("SET TRANSACTION ISOLATION LEVEL REPEATABLE READ")
5125            .execute(&mut *transaction)
5126            .await
5127            .context("failed to stabilize execution payload snapshot")?;
5128        sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE MODE")
5129            .execute(&mut *transaction)
5130            .await
5131            .context("failed to stabilize execution payload check")?;
5132        let marker = sqlx::query_scalar::<_, i16>(
5133            "SELECT version FROM execution_schema_version WHERE component = $1",
5134        )
5135        .bind(EXECUTION_PAYLOAD_COMPONENT)
5136        .fetch_optional(&mut *transaction)
5137        .await
5138        .context("failed to read execution payload marker")?;
5139        let deployment_id = match (marker, keys) {
5140            (None, _) => {
5141                let state = sqlx::query_scalar::<_, bool>(
5142                    "SELECT EXISTS (SELECT 1 FROM execution_payload_state) \
5143                     OR EXISTS (SELECT 1 FROM execution_transaction_hash \
5144                                WHERE sealed_transaction IS NOT NULL)",
5145                )
5146                .fetch_one(&mut *transaction)
5147                .await
5148                .context("failed to check legacy payload state")?;
5149                anyhow::ensure!(!state, "Legacy payload storage contains protected state");
5150                None
5151            }
5152            (Some(version), Some(keys)) => {
5153                anyhow::ensure!(
5154                    version == EXECUTION_PAYLOAD_PROTOCOL_VERSION,
5155                    "Unsupported execution payload protection version {version}"
5156                );
5157                let state_row = sqlx::query(
5158                    "SELECT deployment_id, protocol_version, operation, active_key_id \
5159                     FROM execution_payload_state WHERE component = 'signed_transactions' FOR SHARE",
5160                )
5161                .fetch_optional(&mut *transaction)
5162                .await
5163                .context("failed to lock execution payload state")?
5164                .ok_or_else(|| anyhow::anyhow!("Execution payload state is missing"))?;
5165                let state = execution_payload_state_from_row(&state_row)?;
5166                validate_execution_payload_state(&state, keys)?;
5167                anyhow::ensure!(
5168                    state.operation == "ready",
5169                    "Execution payload storage is not ready"
5170                );
5171                Some(state.deployment_id)
5172            }
5173            (Some(_), None) => anyhow::bail!(
5174                "Execution payload protection is active, but no payload key is configured"
5175            ),
5176        };
5177
5178        let mut cursor = 0_i64;
5179        let mut plaintext_rows = 0_u64;
5180        let mut original_rows = 0_u64;
5181        let mut replacement_rows = 0_u64;
5182        let mut authenticated_rows = 0_u64;
5183        let mut key_ids = BTreeSet::new();
5184
5185        loop {
5186            let rows = sqlx::query_as::<_, ExecutionTransactionHashRow>(
5187                "
5188                SELECT
5189                    id, intent_id, chain_id, transaction_hash, payload_expected,
5190                    raw_transaction, sealed_transaction, status, block_number, block_hash,
5191                    receipt_success, gas_used, effective_gas_price, current
5192                FROM execution_transaction_hash
5193                WHERE id > $1
5194                ORDER BY id
5195                LIMIT $2
5196                ",
5197            )
5198            .bind(cursor)
5199            .bind(batch_size)
5200            .fetch_all(&mut *transaction)
5201            .await
5202            .context("failed to load execution payload check batch")?;
5203
5204            if rows.is_empty() {
5205                break;
5206            }
5207
5208            for hash in &rows {
5209                cursor = hash.id;
5210                plaintext_rows += u64::from(hash.raw_transaction.is_some());
5211                if !hash.payload_expected {
5212                    replacement_rows += 1;
5213                    anyhow::ensure!(
5214                        hash.raw_transaction.is_none() && hash.sealed_transaction.is_none(),
5215                        "Replacement execution transaction {} retains signed bytes",
5216                        hash.id
5217                    );
5218                    continue;
5219                }
5220                original_rows += 1;
5221                let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
5222                let raw_transaction = if let (Some(keys), Some(deployment_id)) =
5223                    (keys, deployment_id.as_deref())
5224                {
5225                    anyhow::ensure!(
5226                        hash.raw_transaction.is_none(),
5227                        "Protected execution transaction {} contains plaintext",
5228                        hash.id
5229                    );
5230                    let envelope = hash.sealed_transaction.as_deref().ok_or_else(|| {
5231                        anyhow::anyhow!(
5232                            "Protected execution transaction {} has no envelope",
5233                            hash.id
5234                        )
5235                    })?;
5236                    let key_id = envelope_key_id(envelope)?;
5237                    anyhow::ensure!(
5238                        keys.contains_key(&key_id),
5239                        "Execution transaction {} requires an unavailable payload key",
5240                        hash.id
5241                    );
5242                    key_ids.insert(alloy::hex::encode(key_id));
5243                    let context = payload_context(&intent, hash, deployment_id)?;
5244                    keys.unseal(envelope, &context)?
5245                } else {
5246                    anyhow::ensure!(
5247                        hash.sealed_transaction.is_none(),
5248                        "Legacy execution transaction {} contains an envelope",
5249                        hash.id
5250                    );
5251                    hash.raw_transaction.clone().ok_or_else(|| {
5252                        anyhow::anyhow!("Legacy execution transaction {} has no plaintext", hash.id)
5253                    })?
5254                };
5255                authenticate_retained_payload(
5256                    &raw_transaction,
5257                    &intent,
5258                    hash,
5259                    deployment_id.as_deref().unwrap_or(""),
5260                )?;
5261
5262                if let Some(policy) = policy
5263                    && retained_payload_requires_policy(&intent, hash, policy)?
5264                {
5265                    authenticate_payload(
5266                        &raw_transaction,
5267                        &intent,
5268                        hash,
5269                        policy,
5270                        deployment_id.as_deref().unwrap_or(""),
5271                    )
5272                    .with_context(|| {
5273                        format!(
5274                            "execution intent {} transaction {} violates current execution policy",
5275                            intent.id, hash.transaction_hash
5276                        )
5277                    })?;
5278                }
5279                authenticated_rows += 1;
5280            }
5281        }
5282        let read_roles = sqlx::query_scalar::<_, String>(
5283            "SELECT DISTINCT role_name FROM ( \
5284                 SELECT tableowner AS role_name FROM pg_tables \
5285                 WHERE schemaname = current_schema() AND tablename = 'execution_transaction_hash' \
5286                 UNION \
5287                 SELECT grantee AS role_name FROM information_schema.role_table_grants \
5288                 WHERE table_schema = current_schema() \
5289                   AND table_name = 'execution_transaction_hash' \
5290                   AND privilege_type = 'SELECT' \
5291             ) AS roles ORDER BY role_name",
5292        )
5293        .fetch_all(&mut *transaction)
5294        .await
5295        .context("failed to inspect execution payload read roles")?;
5296        Ok((
5297            ExecutionPayloadCheck {
5298                protected: deployment_id.is_some(),
5299                deployment_id,
5300                plaintext_rows,
5301                original_rows,
5302                replacement_rows,
5303                authenticated_rows,
5304                key_ids: key_ids.into_iter().collect(),
5305                read_roles,
5306            },
5307            transaction,
5308        ))
5309    }
5310
5311    pub(crate) async fn rewrap_execution_payload_storage(
5312        &self,
5313        keys: &PayloadKeySet,
5314        batch_size: i64,
5315    ) -> anyhow::Result<()> {
5316        anyhow::ensure!(
5317            batch_size > 0,
5318            "Execution payload rewrap batch size must be positive"
5319        );
5320        self.begin_execution_payload_rewrap(keys).await?;
5321
5322        loop {
5323            if self
5324                .rewrap_execution_payload_batch(keys, batch_size)
5325                .await?
5326            {
5327                return Ok(());
5328            }
5329        }
5330    }
5331
5332    async fn begin_execution_payload_rewrap(&self, keys: &PayloadKeySet) -> anyhow::Result<()> {
5333        let mut transaction = self
5334            .pool
5335            .begin()
5336            .await
5337            .context("failed to start execution payload rewrap")?;
5338        lock_execution_payload_operation(&mut transaction).await?;
5339        let state_row = sqlx::query(
5340            "SELECT deployment_id, protocol_version, operation, active_key_id \
5341             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
5342        )
5343        .fetch_optional(&mut *transaction)
5344        .await
5345        .context("failed to lock execution payload state for rewrap")?
5346        .ok_or_else(|| anyhow::anyhow!("Execution payload protection is not active"))?;
5347        let state = execution_payload_state_from_row(&state_row)?;
5348        anyhow::ensure!(
5349            state.protocol_version == EXECUTION_PAYLOAD_PROTOCOL_VERSION
5350                && state.deployment_id == keys.deployment_id(),
5351            "Execution payload rewrap context does not match this database"
5352        );
5353
5354        match state.operation.as_str() {
5355            "ready" => {
5356                let current_id: [u8; 32] = state
5357                    .active_key_id
5358                    .as_slice()
5359                    .try_into()
5360                    .context("database active payload key ID is invalid")?;
5361                anyhow::ensure!(
5362                    keys.contains_key(&current_id),
5363                    "Current database payload key is not configured for rewrap"
5364                );
5365                validate_execution_payload_key_inventory(&mut transaction, keys).await?;
5366                if current_id == *keys.active_key_id() {
5367                    transaction
5368                        .commit()
5369                        .await
5370                        .context("failed to complete no-op execution payload rewrap")?;
5371                    return Ok(());
5372                }
5373                sqlx::query(
5374                    "UPDATE execution_payload_state \
5375                     SET operation = 'rewrap', active_key_id = $1, progress_id = 0, updated_at = NOW() \
5376                     WHERE component = 'signed_transactions'",
5377                )
5378                .bind(keys.active_key_id().as_slice())
5379                .execute(&mut *transaction)
5380                .await
5381                .context("failed to record execution payload rewrap target")?;
5382            }
5383            "rewrap" => validate_execution_payload_state(&state, keys)?,
5384            operation => anyhow::bail!(
5385                "Execution payload storage is in {operation} maintenance, not ready for rewrap"
5386            ),
5387        }
5388        sqlx::query(
5389            "INSERT INTO execution_payload_key_state (key_id, seals) VALUES ($1, 0) \
5390             ON CONFLICT (key_id) DO NOTHING",
5391        )
5392        .bind(keys.active_key_id().as_slice())
5393        .execute(&mut *transaction)
5394        .await
5395        .context("failed to initialize rewrap target key state")?;
5396        transaction
5397            .commit()
5398            .await
5399            .context("failed to commit execution payload rewrap state")?;
5400        Ok(())
5401    }
5402
5403    async fn rewrap_execution_payload_batch(
5404        &self,
5405        keys: &PayloadKeySet,
5406        batch_size: i64,
5407    ) -> anyhow::Result<bool> {
5408        let mut transaction = self
5409            .pool
5410            .begin()
5411            .await
5412            .context("failed to start execution payload rewrap batch")?;
5413        lock_execution_payload_operation(&mut transaction).await?;
5414        let state_row = sqlx::query(
5415            "SELECT deployment_id, protocol_version, operation, active_key_id \
5416             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
5417        )
5418        .fetch_optional(&mut *transaction)
5419        .await
5420        .context("failed to lock execution payload rewrap state")?
5421        .ok_or_else(|| anyhow::anyhow!("Execution payload rewrap state is missing"))?;
5422        let state = execution_payload_state_from_row(&state_row)?;
5423        validate_execution_payload_state(&state, keys)?;
5424        if state.operation == "ready" {
5425            transaction
5426                .commit()
5427                .await
5428                .context("failed to complete execution payload rewrap")?;
5429            return Ok(true);
5430        }
5431        anyhow::ensure!(
5432            state.operation == "rewrap",
5433            "Execution payload storage is not rewrapping"
5434        );
5435        let rows = sqlx::query_as::<_, ExecutionTransactionHashRow>(
5436            "
5437            SELECT
5438                id, intent_id, chain_id, transaction_hash, payload_expected,
5439                raw_transaction, sealed_transaction, status, block_number, block_hash,
5440                receipt_success, gas_used, effective_gas_price, current
5441            FROM execution_transaction_hash
5442            WHERE payload_expected
5443              AND substring(sealed_transaction FROM 2 FOR 32) <> $1
5444            ORDER BY id
5445            LIMIT $2
5446            FOR UPDATE
5447            ",
5448        )
5449        .bind(keys.active_key_id().as_slice())
5450        .bind(batch_size)
5451        .fetch_all(&mut *transaction)
5452        .await
5453        .context("failed to load execution payload rewrap batch")?;
5454
5455        if rows.is_empty() {
5456            sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE ROW EXCLUSIVE MODE")
5457                .execute(&mut *transaction)
5458                .await
5459                .context("failed to lock execution payload rewrap completion")?;
5460            let remaining = sqlx::query_scalar::<_, i64>(
5461                "SELECT COUNT(*) FROM execution_transaction_hash \
5462                 WHERE payload_expected \
5463                   AND (raw_transaction IS NOT NULL OR sealed_transaction IS NULL \
5464                        OR substring(sealed_transaction FROM 2 FOR 32) <> $1)",
5465            )
5466            .bind(keys.active_key_id().as_slice())
5467            .fetch_one(&mut *transaction)
5468            .await
5469            .context("failed to verify execution payload rewrap completion")?;
5470            anyhow::ensure!(
5471                remaining == 0,
5472                "Execution payload rewrap left {remaining} row(s)"
5473            );
5474            sqlx::query(
5475                "UPDATE execution_payload_state SET operation = 'ready', updated_at = NOW() \
5476                 WHERE component = 'signed_transactions' AND operation = 'rewrap'",
5477            )
5478            .execute(&mut *transaction)
5479            .await
5480            .context("failed to mark execution payload rewrap complete")?;
5481            transaction
5482                .commit()
5483                .await
5484                .context("failed to commit execution payload rewrap completion")?;
5485            return Ok(true);
5486        }
5487
5488        for hash in rows {
5489            let envelope = hash.sealed_transaction.as_deref().ok_or_else(|| {
5490                anyhow::anyhow!(
5491                    "Execution payload {} has no envelope during rewrap",
5492                    hash.id
5493                )
5494            })?;
5495            anyhow::ensure!(
5496                hash.raw_transaction.is_none(),
5497                "Execution payload {} contains plaintext during rewrap",
5498                hash.id
5499            );
5500            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
5501            let context = payload_context(&intent, &hash, keys.deployment_id())?;
5502            let raw_transaction = keys.unseal(envelope, &context)?;
5503            authenticate_retained_payload(&raw_transaction, &intent, &hash, keys.deployment_id())?;
5504            reserve_execution_payload_seal(&mut transaction, keys.active_key_id()).await?;
5505            let rewrapped = keys.seal(&raw_transaction, &context)?;
5506            let verified = keys.unseal(&rewrapped, &context)?;
5507            authenticate_retained_payload(&verified, &intent, &hash, keys.deployment_id())?;
5508            anyhow::ensure!(
5509                verified == raw_transaction,
5510                "Execution payload {} changed during rewrap",
5511                hash.id
5512            );
5513            let result = sqlx::query(
5514                "UPDATE execution_transaction_hash SET sealed_transaction = $2, updated_at = NOW() \
5515                 WHERE id = $1 AND sealed_transaction = $3 AND raw_transaction IS NULL",
5516            )
5517            .bind(hash.id)
5518            .bind(&rewrapped)
5519            .bind(envelope)
5520            .execute(&mut *transaction)
5521            .await
5522            .context("failed to persist rewrapped execution payload")?;
5523            anyhow::ensure!(
5524                result.rows_affected() == 1,
5525                "Execution payload {} changed during rewrap",
5526                hash.id
5527            );
5528            sqlx::query(
5529                "UPDATE execution_payload_state SET progress_id = $1, updated_at = NOW() \
5530                 WHERE component = 'signed_transactions'",
5531            )
5532            .bind(hash.id)
5533            .execute(&mut *transaction)
5534            .await
5535            .context("failed to record execution payload rewrap progress")?;
5536        }
5537        transaction
5538            .commit()
5539            .await
5540            .context("failed to commit execution payload rewrap batch")?;
5541        Ok(false)
5542    }
5543
5544    pub(crate) async fn rollback_execution_payload_storage(
5545        &self,
5546        keys: &PayloadKeySet,
5547        batch_size: i64,
5548    ) -> anyhow::Result<()> {
5549        anyhow::ensure!(
5550            batch_size > 0,
5551            "Execution payload rollback batch size must be positive"
5552        );
5553        self.begin_execution_payload_rollback(keys).await?;
5554
5555        loop {
5556            if self
5557                .rollback_execution_payload_batch(keys, batch_size)
5558                .await?
5559            {
5560                return Ok(());
5561            }
5562        }
5563    }
5564
5565    async fn begin_execution_payload_rollback(&self, keys: &PayloadKeySet) -> anyhow::Result<()> {
5566        let mut transaction = self
5567            .pool
5568            .begin()
5569            .await
5570            .context("failed to start execution payload rollback")?;
5571        lock_execution_payload_operation(&mut transaction).await?;
5572        let state_row = sqlx::query(
5573            "SELECT deployment_id, protocol_version, operation, active_key_id \
5574             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
5575        )
5576        .fetch_optional(&mut *transaction)
5577        .await
5578        .context("failed to lock execution payload state for rollback")?
5579        .ok_or_else(|| anyhow::anyhow!("Execution payload protection is not active"))?;
5580        let state = execution_payload_state_from_row(&state_row)?;
5581        validate_execution_payload_state(&state, keys)?;
5582        match state.operation.as_str() {
5583            "ready" => {
5584                sqlx::query(
5585                    "ALTER TABLE execution_transaction_hash \
5586                     DROP CONSTRAINT IF EXISTS execution_transaction_payload_protected_check",
5587                )
5588                .execute(&mut *transaction)
5589                .await
5590                .context("failed to open execution payload rollback representation")?;
5591                sqlx::query(
5592                    "UPDATE execution_payload_state \
5593                     SET operation = 'rollback', progress_id = 0, updated_at = NOW() \
5594                     WHERE component = 'signed_transactions'",
5595                )
5596                .execute(&mut *transaction)
5597                .await
5598                .context("failed to record execution payload rollback state")?;
5599            }
5600            "rollback" => {}
5601            operation => anyhow::bail!(
5602                "Execution payload storage is in {operation} maintenance, not ready for rollback"
5603            ),
5604        }
5605        transaction
5606            .commit()
5607            .await
5608            .context("failed to commit execution payload rollback state")?;
5609        Ok(())
5610    }
5611
5612    async fn rollback_execution_payload_batch(
5613        &self,
5614        keys: &PayloadKeySet,
5615        batch_size: i64,
5616    ) -> anyhow::Result<bool> {
5617        let mut transaction = self
5618            .pool
5619            .begin()
5620            .await
5621            .context("failed to start execution payload rollback batch")?;
5622        lock_execution_payload_operation(&mut transaction).await?;
5623        let state_row = sqlx::query(
5624            "SELECT deployment_id, protocol_version, operation, active_key_id \
5625             FROM execution_payload_state WHERE component = 'signed_transactions' FOR UPDATE",
5626        )
5627        .fetch_optional(&mut *transaction)
5628        .await
5629        .context("failed to lock execution payload rollback state")?
5630        .ok_or_else(|| anyhow::anyhow!("Execution payload rollback state is missing"))?;
5631        let state = execution_payload_state_from_row(&state_row)?;
5632        validate_execution_payload_state(&state, keys)?;
5633        anyhow::ensure!(
5634            state.operation == "rollback",
5635            "Execution payload storage is not rolling back"
5636        );
5637        let rows = sqlx::query_as::<_, ExecutionTransactionHashRow>(
5638            "
5639            SELECT
5640                id, intent_id, chain_id, transaction_hash, payload_expected,
5641                raw_transaction, sealed_transaction, status, block_number, block_hash,
5642                receipt_success, gas_used, effective_gas_price, current
5643            FROM execution_transaction_hash
5644            WHERE payload_expected AND sealed_transaction IS NOT NULL
5645            ORDER BY id
5646            LIMIT $1
5647            FOR UPDATE
5648            ",
5649        )
5650        .bind(batch_size)
5651        .fetch_all(&mut *transaction)
5652        .await
5653        .context("failed to load execution payload rollback batch")?;
5654
5655        if rows.is_empty() {
5656            sqlx::query("LOCK TABLE execution_transaction_hash IN SHARE ROW EXCLUSIVE MODE")
5657                .execute(&mut *transaction)
5658                .await
5659                .context("failed to lock execution payload rollback completion")?;
5660            let invalid = sqlx::query_scalar::<_, i64>(
5661                "SELECT COUNT(*) FROM execution_transaction_hash \
5662                 WHERE (payload_expected AND (raw_transaction IS NULL OR sealed_transaction IS NOT NULL)) \
5663                    OR (NOT payload_expected AND (raw_transaction IS NOT NULL OR sealed_transaction IS NOT NULL))",
5664            )
5665            .fetch_one(&mut *transaction)
5666            .await
5667            .context("failed to verify execution payload rollback completion")?;
5668            anyhow::ensure!(
5669                invalid == 0,
5670                "Execution payload rollback left {invalid} invalid row(s)"
5671            );
5672
5673            for statement in [
5674                "DROP TRIGGER IF EXISTS execution_transaction_payload_fence ON execution_transaction_hash",
5675                "DELETE FROM execution_payload_state WHERE component = 'signed_transactions'",
5676                "DELETE FROM execution_schema_version WHERE component = 'evm_execution_payload'",
5677            ] {
5678                sqlx::query(statement)
5679                    .execute(&mut *transaction)
5680                    .await
5681                    .context("failed to complete execution payload rollback")?;
5682            }
5683            transaction
5684                .commit()
5685                .await
5686                .context("failed to commit execution payload rollback completion")?;
5687            return Ok(true);
5688        }
5689
5690        for hash in rows {
5691            anyhow::ensure!(
5692                hash.raw_transaction.is_none(),
5693                "Execution payload {} contains both representations during rollback",
5694                hash.id
5695            );
5696            let envelope = hash
5697                .sealed_transaction
5698                .as_deref()
5699                .expect("rollback query requires envelope");
5700            let intent = load_execution_intent(&mut transaction, hash.intent_id).await?;
5701            let context = payload_context(&intent, &hash, keys.deployment_id())?;
5702            let raw_transaction = keys.unseal(envelope, &context)?;
5703            authenticate_retained_payload(&raw_transaction, &intent, &hash, keys.deployment_id())?;
5704            let result = sqlx::query(
5705                "UPDATE execution_transaction_hash SET raw_transaction = $2, updated_at = NOW() \
5706                 WHERE id = $1 AND raw_transaction IS NULL AND sealed_transaction = $3",
5707            )
5708            .bind(hash.id)
5709            .bind(&raw_transaction)
5710            .bind(envelope)
5711            .execute(&mut *transaction)
5712            .await
5713            .context("failed to recreate plaintext execution payload")?;
5714            anyhow::ensure!(
5715                result.rows_affected() == 1,
5716                "Execution payload {} changed during rollback",
5717                hash.id
5718            );
5719            authenticate_retained_payload(&raw_transaction, &intent, &hash, keys.deployment_id())?;
5720            let result = sqlx::query(
5721                "UPDATE execution_transaction_hash SET sealed_transaction = NULL, updated_at = NOW() \
5722                 WHERE id = $1 AND raw_transaction = $2 AND sealed_transaction = $3",
5723            )
5724            .bind(hash.id)
5725            .bind(&raw_transaction)
5726            .bind(envelope)
5727            .execute(&mut *transaction)
5728            .await
5729            .context("failed to clear rolled-back execution payload envelope")?;
5730            anyhow::ensure!(
5731                result.rows_affected() == 1,
5732                "Execution payload {} changed while clearing rollback envelope",
5733                hash.id
5734            );
5735            sqlx::query(
5736                "UPDATE execution_payload_state SET progress_id = $1, updated_at = NOW() \
5737                 WHERE component = 'signed_transactions'",
5738            )
5739            .bind(hash.id)
5740            .execute(&mut *transaction)
5741            .await
5742            .context("failed to record execution payload rollback progress")?;
5743        }
5744        transaction
5745            .commit()
5746            .await
5747            .context("failed to commit execution payload rollback batch")?;
5748        Ok(false)
5749    }
5750
5751    /// Reserves durable ownership of a signer slot and optional client order before signing.
5752    ///
5753    /// # Errors
5754    ///
5755    /// Returns a stage-classified error if the signer or client order is already owned, or
5756    /// persistence fails. A commit-stage error does not prove the transaction rolled back.
5757    pub async fn reserve_execution_intent(
5758        &self,
5759        intent: &ExecutionIntentInsert,
5760    ) -> anyhow::Result<ExecutionIntentRow> {
5761        let (transaction, row) = async {
5762            let chain_id = i32::try_from(intent.chain_id).with_context(|| {
5763                format!("Chain ID {} exceeds PostgreSQL INTEGER", intent.chain_id)
5764            })?;
5765            let created_block = i64::try_from(intent.created_block).with_context(|| {
5766                format!(
5767                    "Execution creation block {} exceeds PostgreSQL BIGINT",
5768                    intent.created_block
5769                )
5770            })?;
5771            let mut transaction = self
5772                .pool
5773                .begin()
5774                .await
5775                .context("failed to start execution intent reservation")?;
5776            Self::lock_execution_signer(&mut transaction, intent.chain_id, &intent.wallet_address)
5777                .await?;
5778            let row = sqlx::query_as::<_, ExecutionIntentRow>(
5779                "
5780                INSERT INTO execution_intent (
5781                    schema_version, chain_id, wallet_address, purpose, status,
5782                    client_order_id, trader_id, strategy_id, account_id, instrument_id,
5783                    pool_address, transaction_to, transaction_input, transaction_value,
5784                    amount_in, created_block
5785                )
5786                VALUES ($1, $2, $3, $4, 'prepared', $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
5787                RETURNING
5788                    id, schema_version, chain_id, wallet_address, nonce, purpose, status,
5789                    client_order_id, trader_id, strategy_id, account_id, instrument_id,
5790                    pool_address, transaction_to, transaction_input, transaction_value,
5791                    amount_in, created_block, acknowledgement_emitted, fill_emitted,
5792                    terminal_emitted, active
5793                ",
5794            )
5795            .bind(EXECUTION_SCHEMA_VERSION)
5796            .bind(chain_id)
5797            .bind(&intent.wallet_address)
5798            .bind(&intent.purpose)
5799            .bind(&intent.client_order_id)
5800            .bind(&intent.trader_id)
5801            .bind(&intent.strategy_id)
5802            .bind(&intent.account_id)
5803            .bind(&intent.instrument_id)
5804            .bind(&intent.pool_address)
5805            .bind(&intent.transaction_to)
5806            .bind(&intent.transaction_input)
5807            .bind(&intent.transaction_value)
5808            .bind(&intent.amount_in)
5809            .bind(created_block)
5810            .fetch_one(&mut *transaction)
5811            .await
5812            .context("failed to insert execution intent reservation")?;
5813
5814            sqlx::query(
5815                "
5816                INSERT INTO execution_transaction_transition (
5817                    intent_id, transition_key, to_status, block_number
5818                ) VALUES ($1, 'prepared', 'prepared', $2)
5819                ",
5820            )
5821            .bind(row.id)
5822            .bind(created_block)
5823            .execute(&mut *transaction)
5824            .await
5825            .context("failed to record prepared execution intent")?;
5826
5827            Ok::<_, anyhow::Error>((transaction, row))
5828        }
5829        .await
5830        .map_err(|source| {
5831            anyhow::Error::new(ExecutionIntentReservationError {
5832                stage: ExecutionIntentReservationStage::BeforeCommit,
5833                source,
5834            })
5835        })?;
5836        transaction.commit().await.map_err(|e| {
5837            anyhow::Error::new(ExecutionIntentReservationError {
5838                stage: ExecutionIntentReservationStage::Commit,
5839                source: anyhow::Error::new(e)
5840                    .context("failed to commit execution intent reservation"),
5841            })
5842        })?;
5843        Ok(row)
5844    }
5845
5846    /// Assigns the signer nonce to a prepared execution intent.
5847    ///
5848    /// Repeating the same assignment is idempotent. A different nonce or non-prepared state
5849    /// fails closed.
5850    ///
5851    /// # Errors
5852    ///
5853    /// Returns an error if the intent cannot own the nonce or persistence fails.
5854    pub async fn assign_execution_intent_nonce(
5855        &self,
5856        intent_id: i64,
5857        nonce: u64,
5858    ) -> anyhow::Result<()> {
5859        let nonce_db = i64::try_from(nonce)
5860            .with_context(|| format!("Execution nonce {nonce} exceeds PostgreSQL BIGINT"))?;
5861        let result = sqlx::query(
5862            "
5863            UPDATE execution_intent
5864            SET nonce = $2, updated_at = NOW()
5865            WHERE id = $1
5866              AND status = 'prepared'
5867              AND (nonce IS NULL OR nonce = $2)
5868            ",
5869        )
5870        .bind(intent_id)
5871        .bind(nonce_db)
5872        .execute(&self.pool)
5873        .await
5874        .map_err(|e| anyhow::anyhow!("Failed to assign nonce {nonce} to execution intent: {e}"))?;
5875        anyhow::ensure!(
5876            result.rows_affected() == 1,
5877            "Execution intent {intent_id} is not prepared for nonce {nonce}"
5878        );
5879        Ok(())
5880    }
5881
5882    /// Assigns the canonical nonce and its authorizing verification evidence atomically.
5883    ///
5884    /// # Errors
5885    ///
5886    /// Returns an error if the durable nonce ledger, intent ownership, manifest identity, or
5887    /// evidence is inconsistent, or if persistence fails.
5888    pub(crate) async fn assign_execution_intent_nonce_verified(
5889        &self,
5890        assignment: &ExecutionNonceAssignment<'_>,
5891    ) -> anyhow::Result<()> {
5892        anyhow::ensure!(
5893            !assignment.decisions.is_empty(),
5894            "Verified nonce assignment requires decision evidence"
5895        );
5896        anyhow::ensure!(
5897            assignment.provider_ids.len() == 3 && assignment.operator_ids.len() == 3,
5898            "Verified nonce assignment requires exactly three provider and operator IDs"
5899        );
5900        anyhow::ensure!(
5901            assignment.failure_domain_ids.len() >= 3,
5902            "Verified nonce assignment requires the configured failure domains"
5903        );
5904        let chain_id = i32::try_from(assignment.chain_id)
5905            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
5906        let nonce =
5907            i64::try_from(assignment.nonce).context("Execution nonce exceeds PostgreSQL BIGINT")?;
5908        let mut transaction = self
5909            .pool
5910            .begin()
5911            .await
5912            .map_err(|e| anyhow::anyhow!("Failed to start verified nonce assignment: {e}"))?;
5913        let (manifest_version, manifest_digest, next_nonce, revision) =
5914            sqlx::query_as::<_, (String, String, i64, i64)>(
5915                "
5916                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
5917                FROM execution_verification_nonce
5918                WHERE chain_id = $1 AND wallet_address = $2
5919                FOR UPDATE
5920                ",
5921            )
5922            .bind(chain_id)
5923            .bind(assignment.wallet_address)
5924            .fetch_optional(&mut *transaction)
5925            .await
5926            .map_err(|e| anyhow::anyhow!("Failed to lock canonical nonce ledger: {e}"))?
5927            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
5928        anyhow::ensure!(
5929            manifest_version == assignment.manifest_version
5930                && manifest_digest == assignment.manifest_digest,
5931            "Verified nonce assignment manifest identity changed"
5932        );
5933        anyhow::ensure!(
5934            next_nonce == nonce,
5935            "Execution nonce {} does not match canonical nonce {next_nonce}",
5936            assignment.nonce
5937        );
5938
5939        let (intent_chain_id, intent_wallet, intent_nonce, intent_status, intent_active) =
5940            sqlx::query_as::<_, (i32, String, Option<i64>, String, bool)>(
5941                "
5942            SELECT chain_id, wallet_address, nonce, status, active
5943            FROM execution_intent
5944            WHERE id = $1
5945            FOR UPDATE
5946            ",
5947            )
5948            .bind(assignment.intent_id)
5949            .fetch_optional(&mut *transaction)
5950            .await
5951            .map_err(|e| {
5952                anyhow::anyhow!("Failed to lock execution intent for nonce assignment: {e}")
5953            })?
5954            .ok_or_else(|| {
5955                anyhow::anyhow!("Execution intent {} was not found", assignment.intent_id)
5956            })?;
5957        anyhow::ensure!(
5958            intent_chain_id == chain_id
5959                && intent_wallet == assignment.wallet_address
5960                && intent_status == "prepared"
5961                && intent_active
5962                && intent_nonce.is_none_or(|assigned| assigned == nonce),
5963            "Execution intent {} cannot own canonical nonce {}",
5964            assignment.intent_id,
5965            assignment.nonce
5966        );
5967
5968        for (index, decision) in assignment.decisions.iter().enumerate() {
5969            let height_start = decision
5970                .height_start
5971                .map(i64::try_from)
5972                .transpose()
5973                .context("Verification height exceeds PostgreSQL BIGINT")?;
5974            let height_end = decision
5975                .height_end
5976                .map(i64::try_from)
5977                .transpose()
5978                .context("Verification height exceeds PostgreSQL BIGINT")?;
5979            let transition_key = format!(
5980                "pre_sign:{}:{}:{index}",
5981                assignment.intent_id, decision.read_class
5982            );
5983            sqlx::query(
5984                "
5985                INSERT INTO execution_verification_decision (
5986                    intent_id, nonce, decision_class, read_class, height_start, height_end,
5987                    manifest_version, manifest_digest, provider_ids, operator_ids,
5988                    failure_domain_ids, response_class, normalized_value_digest,
5989                    nonce_revision, outcome, transition_key
5990                )
5991                VALUES (
5992                    $1, $2, 'pre_sign', $3, $4, $5, $6, $7, $8, $9, $10,
5993                    'all_valid', $11, $12, 'verified', $13
5994                )
5995                ",
5996            )
5997            .bind(assignment.intent_id)
5998            .bind(nonce)
5999            .bind(decision.read_class)
6000            .bind(height_start)
6001            .bind(height_end)
6002            .bind(assignment.manifest_version)
6003            .bind(assignment.manifest_digest)
6004            .bind(assignment.provider_ids)
6005            .bind(assignment.operator_ids)
6006            .bind(assignment.failure_domain_ids)
6007            .bind(&decision.normalized_value_digest)
6008            .bind(revision)
6009            .bind(transition_key)
6010            .execute(&mut *transaction)
6011            .await
6012            .map_err(|e| anyhow::anyhow!("Failed to persist pre-sign verification: {e}"))?;
6013        }
6014
6015        let result = sqlx::query(
6016            "
6017            UPDATE execution_intent
6018            SET nonce = $2, updated_at = NOW()
6019            WHERE id = $1
6020              AND status = 'prepared'
6021              AND active
6022              AND (nonce IS NULL OR nonce = $2)
6023            ",
6024        )
6025        .bind(assignment.intent_id)
6026        .bind(nonce)
6027        .execute(&mut *transaction)
6028        .await
6029        .map_err(|e| anyhow::anyhow!("Failed to assign verified execution nonce: {e}"))?;
6030        anyhow::ensure!(
6031            result.rows_affected() == 1,
6032            "Execution intent {} is not prepared for canonical nonce {}",
6033            assignment.intent_id,
6034            assignment.nonce
6035        );
6036        transaction
6037            .commit()
6038            .await
6039            .map_err(|e| anyhow::anyhow!("Failed to commit verified nonce assignment: {e}"))?;
6040        Ok(())
6041    }
6042
6043    /// Appends one verified decision batch before an action on an existing active intent.
6044    pub(crate) async fn record_execution_verification_batch(
6045        &self,
6046        batch: &ExecutionVerificationBatch<'_>,
6047    ) -> anyhow::Result<()> {
6048        anyhow::ensure!(
6049            !batch.decision_class.trim().is_empty() && !batch.decisions.is_empty(),
6050            "Verified action requires a decision class and evidence"
6051        );
6052        anyhow::ensure!(
6053            batch.provider_ids.len() == 3
6054                && batch.operator_ids.len() == 3
6055                && batch.failure_domain_ids.len() >= 3,
6056            "Verified action requires the configured provider identities"
6057        );
6058        let chain_id = i32::try_from(batch.chain_id)
6059            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
6060        let nonce =
6061            i64::try_from(batch.nonce).context("Execution nonce exceeds PostgreSQL BIGINT")?;
6062        let mut transaction = self
6063            .pool
6064            .begin()
6065            .await
6066            .map_err(|e| anyhow::anyhow!("Failed to start verified action evidence: {e}"))?;
6067        let (manifest_version, manifest_digest, revision) =
6068            sqlx::query_as::<_, (String, String, i64)>(
6069                "
6070                SELECT manifest_version, manifest_digest, revision
6071                FROM execution_verification_nonce
6072                WHERE chain_id = $1 AND wallet_address = $2
6073                FOR SHARE
6074                ",
6075            )
6076            .bind(chain_id)
6077            .bind(batch.wallet_address)
6078            .fetch_optional(&mut *transaction)
6079            .await
6080            .map_err(|e| anyhow::anyhow!("Failed to read verified action nonce ledger: {e}"))?
6081            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
6082        anyhow::ensure!(
6083            manifest_version == batch.manifest_version && manifest_digest == batch.manifest_digest,
6084            "Verified action manifest identity changed"
6085        );
6086        let intent_nonce = sqlx::query_scalar::<_, Option<i64>>(
6087            "
6088            SELECT nonce
6089            FROM execution_intent
6090            WHERE id = $1 AND chain_id = $2 AND wallet_address = $3 AND active
6091            FOR UPDATE
6092            ",
6093        )
6094        .bind(batch.intent_id)
6095        .bind(chain_id)
6096        .bind(batch.wallet_address)
6097        .fetch_optional(&mut *transaction)
6098        .await
6099        .map_err(|e| anyhow::anyhow!("Failed to lock intent for verified action: {e}"))?
6100        .ok_or_else(|| anyhow::anyhow!("Active verified-action intent was not found"))?;
6101        anyhow::ensure!(
6102            intent_nonce == Some(nonce),
6103            "Verified action nonce does not match the active intent"
6104        );
6105        let attempt = sqlx::query_scalar::<_, i64>(
6106            "
6107            SELECT COUNT(*)
6108            FROM execution_verification_decision
6109            WHERE intent_id = $1 AND decision_class = $2
6110            ",
6111        )
6112        .bind(batch.intent_id)
6113        .bind(batch.decision_class)
6114        .fetch_one(&mut *transaction)
6115        .await
6116        .map_err(|e| anyhow::anyhow!("Failed to number verified action evidence: {e}"))?;
6117
6118        for (index, decision) in batch.decisions.iter().enumerate() {
6119            let height_start = decision
6120                .height_start
6121                .map(i64::try_from)
6122                .transpose()
6123                .context("Verification height exceeds PostgreSQL BIGINT")?;
6124            let height_end = decision
6125                .height_end
6126                .map(i64::try_from)
6127                .transpose()
6128                .context("Verification height exceeds PostgreSQL BIGINT")?;
6129            let transition_key = format!(
6130                "{}:{}:{attempt}:{}:{index}",
6131                batch.decision_class, batch.intent_id, decision.read_class
6132            );
6133            sqlx::query(
6134                "
6135                INSERT INTO execution_verification_decision (
6136                    intent_id, nonce, decision_class, read_class, height_start, height_end,
6137                    manifest_version, manifest_digest, provider_ids, operator_ids,
6138                    failure_domain_ids, response_class, normalized_value_digest,
6139                    nonce_revision, outcome, transition_key
6140                )
6141                VALUES (
6142                    $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11,
6143                    'all_valid', $12, $13, 'verified', $14
6144                )
6145                ",
6146            )
6147            .bind(batch.intent_id)
6148            .bind(nonce)
6149            .bind(batch.decision_class)
6150            .bind(decision.read_class)
6151            .bind(height_start)
6152            .bind(height_end)
6153            .bind(batch.manifest_version)
6154            .bind(batch.manifest_digest)
6155            .bind(batch.provider_ids)
6156            .bind(batch.operator_ids)
6157            .bind(batch.failure_domain_ids)
6158            .bind(&decision.normalized_value_digest)
6159            .bind(revision)
6160            .bind(transition_key)
6161            .execute(&mut *transaction)
6162            .await
6163            .map_err(|e| anyhow::anyhow!("Failed to persist verified action evidence: {e}"))?;
6164        }
6165        transaction
6166            .commit()
6167            .await
6168            .map_err(|e| anyhow::anyhow!("Failed to commit verified action evidence: {e}"))?;
6169        Ok(())
6170    }
6171
6172    pub(crate) async fn load_execution_replacement_cursor(
6173        &self,
6174        intent_id: i64,
6175        chain_id: u32,
6176        wallet_address: &str,
6177        nonce: u64,
6178        manifest_digest: &str,
6179    ) -> anyhow::Result<Option<ExecutionVerifiedHeader>> {
6180        let chain_id = i32::try_from(chain_id)
6181            .context("Replacement scan chain ID exceeds PostgreSQL INTEGER")?;
6182        let nonce =
6183            i64::try_from(nonce).context("Replacement scan nonce exceeds PostgreSQL BIGINT")?;
6184        let row = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
6185            "
6186            SELECT
6187                h.number, h.hash, h.parent_hash, h.timestamp, h.base_fee_per_gas,
6188                s.manifest_digest
6189            FROM execution_replacement_scan AS s
6190            JOIN execution_verified_finalized_header AS h
6191              ON h.chain_id = s.chain_id
6192             AND h.wallet_address = s.wallet_address
6193             AND h.number = s.finalized_cursor_number
6194             AND h.hash = s.finalized_cursor_hash
6195            WHERE s.intent_id = $1
6196              AND s.chain_id = $2
6197              AND s.wallet_address = $3
6198              AND s.nonce = $4
6199            ",
6200        )
6201        .bind(intent_id)
6202        .bind(chain_id)
6203        .bind(wallet_address)
6204        .bind(nonce)
6205        .fetch_optional(&self.pool)
6206        .await
6207        .context("failed to load replacement scan cursor")?;
6208        row.map(
6209            |(number, hash, parent_hash, timestamp, base_fee, stored_digest)| {
6210                anyhow::ensure!(
6211                    stored_digest == manifest_digest,
6212                    "Replacement scan manifest identity changed"
6213                );
6214                Ok(ExecutionVerifiedHeader {
6215                    number: u64::try_from(number)
6216                        .context("Replacement scan cursor number is negative")?,
6217                    hash,
6218                    parent_hash,
6219                    timestamp: u64::try_from(timestamp)
6220                        .context("Replacement scan cursor timestamp is negative")?,
6221                    base_fee_per_gas: base_fee
6222                        .map(|value| {
6223                            value.parse::<u128>().map_err(|_| {
6224                                anyhow::anyhow!("Replacement scan cursor base fee is invalid")
6225                            })
6226                        })
6227                        .transpose()?,
6228                })
6229            },
6230        )
6231        .transpose()
6232    }
6233
6234    pub(crate) async fn record_execution_replacement_scan(
6235        &self,
6236        scan: &ExecutionReplacementScan<'_>,
6237    ) -> anyhow::Result<()> {
6238        anyhow::ensure!(
6239            !scan.decisions.is_empty()
6240                && scan.provider_ids.len() == 3
6241                && scan.operator_ids.len() == 3
6242                && scan.failure_domain_ids.len() >= 3,
6243            "Replacement scan verification evidence is incomplete"
6244        );
6245        let chain_id = i32::try_from(scan.chain_id)
6246            .context("Replacement scan chain ID exceeds PostgreSQL INTEGER")?;
6247        let nonce = i64::try_from(scan.nonce)
6248            .context("Replacement scan nonce exceeds PostgreSQL BIGINT")?;
6249        let mut transaction = self
6250            .pool
6251            .begin()
6252            .await
6253            .context("failed to start verified replacement scan transition")?;
6254        let (stored_version, stored_digest, stored_nonce, revision) =
6255            sqlx::query_as::<_, (String, String, i64, i64)>(
6256                "
6257                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
6258                FROM execution_verification_nonce
6259                WHERE chain_id = $1 AND wallet_address = $2
6260                FOR SHARE
6261                ",
6262            )
6263            .bind(chain_id)
6264            .bind(scan.wallet_address)
6265            .fetch_optional(&mut *transaction)
6266            .await
6267            .context("failed to read replacement scan nonce ledger")?
6268            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
6269        anyhow::ensure!(
6270            stored_version == scan.manifest_version
6271                && stored_digest == scan.manifest_digest
6272                && stored_nonce == nonce,
6273            "Replacement scan conflicts with the canonical nonce or manifest ledger"
6274        );
6275        let current_status = sqlx::query_scalar::<_, String>(
6276            "
6277            SELECT status
6278            FROM execution_intent
6279            WHERE id = $1 AND chain_id = $2 AND wallet_address = $3
6280              AND nonce = $4 AND active
6281            FOR UPDATE
6282            ",
6283        )
6284        .bind(scan.intent_id)
6285        .bind(chain_id)
6286        .bind(scan.wallet_address)
6287        .bind(nonce)
6288        .fetch_optional(&mut *transaction)
6289        .await
6290        .context("failed to lock the replacement scan intent")?
6291        .ok_or_else(|| anyhow::anyhow!("Active replacement scan intent was not found"))?;
6292
6293        if let Some(cursor) = scan.finalized_cursor {
6294            let number = i64::try_from(cursor.number)
6295                .context("Replacement scan cursor exceeds PostgreSQL BIGINT")?;
6296            let durable_hash = sqlx::query_scalar::<_, String>(
6297                "
6298                SELECT hash
6299                FROM execution_verified_finalized_header
6300                WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
6301                ",
6302            )
6303            .bind(chain_id)
6304            .bind(scan.wallet_address)
6305            .bind(number)
6306            .fetch_optional(&mut *transaction)
6307            .await
6308            .context("failed to validate replacement cursor against finalized headers")?
6309            .ok_or_else(|| {
6310                anyhow::anyhow!(
6311                    "Replacement scan cursor {} is not durably finalized",
6312                    cursor.number
6313                )
6314            })?;
6315            anyhow::ensure!(
6316                durable_hash == cursor.hash,
6317                "Replacement scan cursor conflicts with the finalized header ledger"
6318            );
6319            let existing = sqlx::query_as::<_, (i64, String)>(
6320                "
6321                SELECT finalized_cursor_number, finalized_cursor_hash
6322                FROM execution_replacement_scan
6323                WHERE intent_id = $1
6324                FOR UPDATE
6325                ",
6326            )
6327            .bind(scan.intent_id)
6328            .fetch_optional(&mut *transaction)
6329            .await
6330            .context("failed to lock replacement scan progress")?;
6331
6332            if let Some((stored_number, stored_hash)) = existing {
6333                anyhow::ensure!(
6334                    number > stored_number
6335                        || (number == stored_number && cursor.hash == stored_hash),
6336                    "Replacement scan cursor regressed or changed"
6337                );
6338            }
6339            sqlx::query(
6340                "
6341                INSERT INTO execution_replacement_scan (
6342                    intent_id, chain_id, wallet_address, nonce,
6343                    finalized_cursor_number, finalized_cursor_hash, manifest_digest
6344                ) VALUES ($1, $2, $3, $4, $5, $6, $7)
6345                ON CONFLICT (intent_id) DO UPDATE SET
6346                    finalized_cursor_number = EXCLUDED.finalized_cursor_number,
6347                    finalized_cursor_hash = EXCLUDED.finalized_cursor_hash,
6348                    updated_at = NOW()
6349                ",
6350            )
6351            .bind(scan.intent_id)
6352            .bind(chain_id)
6353            .bind(scan.wallet_address)
6354            .bind(nonce)
6355            .bind(number)
6356            .bind(&cursor.hash)
6357            .bind(scan.manifest_digest)
6358            .execute(&mut *transaction)
6359            .await
6360            .context("failed to persist replacement scan cursor")?;
6361        }
6362
6363        let attempt = sqlx::query_scalar::<_, i64>(
6364            "
6365            SELECT COUNT(*)
6366            FROM execution_verification_decision
6367            WHERE intent_id = $1 AND decision_class = 'replacement_scan'
6368            ",
6369        )
6370        .bind(scan.intent_id)
6371        .fetch_one(&mut *transaction)
6372        .await
6373        .context("failed to number replacement scan evidence")?;
6374        for (index, decision) in scan.decisions.iter().enumerate() {
6375            let height_start = decision
6376                .height_start
6377                .map(i64::try_from)
6378                .transpose()
6379                .context("Replacement scan height exceeds PostgreSQL BIGINT")?;
6380            let height_end = decision
6381                .height_end
6382                .map(i64::try_from)
6383                .transpose()
6384                .context("Replacement scan height exceeds PostgreSQL BIGINT")?;
6385            let transition_key = format!(
6386                "replacement_scan:{}:{attempt}:{}:{index}",
6387                scan.intent_id, decision.read_class
6388            );
6389            sqlx::query(
6390                "
6391                INSERT INTO execution_verification_decision (
6392                    intent_id, nonce, decision_class, read_class, height_start, height_end,
6393                    manifest_version, manifest_digest, provider_ids, operator_ids,
6394                    failure_domain_ids, response_class, normalized_value_digest,
6395                    nonce_revision, outcome, transition_key
6396                ) VALUES (
6397                    $1, $2, 'replacement_scan', $3, $4, $5, $6, $7, $8, $9, $10,
6398                    'all_valid', $11, $12, 'verified', $13
6399                )
6400                ",
6401            )
6402            .bind(scan.intent_id)
6403            .bind(nonce)
6404            .bind(decision.read_class)
6405            .bind(height_start)
6406            .bind(height_end)
6407            .bind(scan.manifest_version)
6408            .bind(scan.manifest_digest)
6409            .bind(scan.provider_ids)
6410            .bind(scan.operator_ids)
6411            .bind(scan.failure_domain_ids)
6412            .bind(&decision.normalized_value_digest)
6413            .bind(revision)
6414            .bind(transition_key)
6415            .execute(&mut *transaction)
6416            .await
6417            .context("failed to persist replacement scan evidence")?;
6418        }
6419
6420        if let Some(transaction_hash) = scan.matched_transaction_hash {
6421            anyhow::ensure!(
6422                execution_transition_allowed(&current_status, TransactionStatus::Replaced),
6423                "Intent cannot attach a verified replacement from status {current_status}"
6424            );
6425            let (hash_id, payload_expected, already_current) =
6426                sqlx::query_as::<_, (i64, bool, bool)>(
6427                    "
6428                    SELECT id, payload_expected, current
6429                    FROM execution_transaction_hash
6430                    WHERE intent_id = $1 AND chain_id = $2 AND transaction_hash = $3
6431                    FOR UPDATE
6432                    ",
6433                )
6434                .bind(scan.intent_id)
6435                .bind(chain_id)
6436                .bind(transaction_hash)
6437                .fetch_optional(&mut *transaction)
6438                .await
6439                .context("failed to lock authenticated replacement payload")?
6440                .ok_or_else(|| {
6441                    anyhow::anyhow!(
6442                        "Replacement transaction {transaction_hash} has no retained payload"
6443                    )
6444                })?;
6445            anyhow::ensure!(
6446                payload_expected,
6447                "Replacement transaction {transaction_hash} is not authenticated"
6448            );
6449
6450            if !already_current {
6451                sqlx::query(
6452                    "
6453                    UPDATE execution_transaction_hash
6454                    SET current = FALSE, status = 'replaced', updated_at = NOW()
6455                    WHERE intent_id = $1 AND current
6456                    ",
6457                )
6458                .bind(scan.intent_id)
6459                .execute(&mut *transaction)
6460                .await
6461                .context("failed to retire the prior transaction hash")?;
6462                sqlx::query(
6463                    "
6464                    UPDATE execution_transaction_hash
6465                    SET current = TRUE, status = 'replaced', updated_at = NOW()
6466                    WHERE id = $1
6467                    ",
6468                )
6469                .bind(hash_id)
6470                .execute(&mut *transaction)
6471                .await
6472                .context("failed to attach the authenticated replacement hash")?;
6473                sqlx::query(
6474                    "UPDATE execution_intent SET status = 'replaced', updated_at = NOW() WHERE id = $1",
6475                )
6476                .bind(scan.intent_id)
6477                .execute(&mut *transaction)
6478                .await
6479                .context("failed to mark the execution intent replaced")?;
6480                sqlx::query(
6481                    "
6482                    INSERT INTO execution_transaction_transition (
6483                        intent_id, transaction_hash_id, transition_key, from_status, to_status
6484                    ) VALUES ($1, $2, $3, $4, 'replaced')
6485                    ON CONFLICT (intent_id, transition_key) DO NOTHING
6486                    ",
6487                )
6488                .bind(scan.intent_id)
6489                .bind(hash_id)
6490                .bind(format!("replacement_verified:{transaction_hash}"))
6491                .bind(current_status)
6492                .execute(&mut *transaction)
6493                .await
6494                .context("failed to record the verified replacement transition")?;
6495            }
6496        }
6497
6498        transaction
6499            .commit()
6500            .await
6501            .context("failed to commit verified replacement scan")?;
6502        Ok(())
6503    }
6504
6505    /// Releases an intent when no broadcast attempt can have occurred.
6506    ///
6507    /// # Errors
6508    ///
6509    /// Returns an error if the intent advanced to broadcast or persistence fails.
6510    pub async fn mark_execution_intent_recoverable(&self, intent_id: i64) -> anyhow::Result<()> {
6511        let mut transaction = self.pool.begin().await.map_err(|e| {
6512            anyhow::anyhow!("Failed to start recoverable execution transition: {e}")
6513        })?;
6514        let current_status = sqlx::query_scalar::<_, String>(
6515            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
6516        )
6517        .bind(intent_id)
6518        .fetch_optional(&mut *transaction)
6519        .await
6520        .map_err(|e| anyhow::anyhow!("Failed to lock recoverable execution intent: {e}"))?
6521        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
6522        anyhow::ensure!(
6523            current_status == "prepared",
6524            "Execution intent {intent_id} is {current_status}, not recoverable before signing"
6525        );
6526        let result = sqlx::query(
6527            "
6528            UPDATE execution_intent
6529            SET status = 'recoverable', active = FALSE, updated_at = NOW()
6530            WHERE id = $1 AND status = 'prepared'
6531            ",
6532        )
6533        .bind(intent_id)
6534        .execute(&mut *transaction)
6535        .await
6536        .map_err(|e| anyhow::anyhow!("Failed to mark execution intent recoverable: {e}"))?;
6537        anyhow::ensure!(
6538            result.rows_affected() == 1,
6539            "Execution intent {intent_id} is not recoverable from preparation"
6540        );
6541        sqlx::query(
6542            "
6543            INSERT INTO execution_transaction_transition (
6544                intent_id, transition_key, from_status, to_status
6545            ) VALUES ($1, 'recoverable', $2, 'recoverable')
6546            ON CONFLICT (intent_id, transition_key) DO NOTHING
6547            ",
6548        )
6549        .bind(intent_id)
6550        .bind(current_status)
6551        .execute(&mut *transaction)
6552        .await
6553        .map_err(|e| anyhow::anyhow!("Failed to record recoverable transition: {e}"))?;
6554        transaction
6555            .commit()
6556            .await
6557            .map_err(|e| anyhow::anyhow!("Failed to commit recoverable transition: {e}"))?;
6558        Ok(())
6559    }
6560
6561    /// Persists a signed transaction and advances its intent before broadcast.
6562    ///
6563    /// # Errors
6564    ///
6565    /// Returns an error if the intent is not prepared, lacks a nonce, conflicts with a stored
6566    /// hash, or persistence fails.
6567    pub async fn add_execution_transaction_hash(
6568        &self,
6569        intent_id: i64,
6570        chain_id: u32,
6571        transaction_hash: &str,
6572        raw_transaction: &[u8],
6573    ) -> anyhow::Result<ExecutionTransactionHashRow> {
6574        self.add_execution_transaction_payload(
6575            intent_id,
6576            chain_id,
6577            transaction_hash,
6578            Some(raw_transaction),
6579            None,
6580        )
6581        .await
6582    }
6583
6584    pub(crate) async fn add_execution_transaction_envelope(
6585        &self,
6586        intent_id: i64,
6587        chain_id: u32,
6588        transaction_hash: &str,
6589        sealed_transaction: &[u8],
6590    ) -> anyhow::Result<ExecutionTransactionHashRow> {
6591        self.add_execution_transaction_payload(
6592            intent_id,
6593            chain_id,
6594            transaction_hash,
6595            None,
6596            Some(sealed_transaction),
6597        )
6598        .await
6599    }
6600
6601    async fn add_execution_transaction_payload(
6602        &self,
6603        intent_id: i64,
6604        chain_id: u32,
6605        transaction_hash: &str,
6606        raw_transaction: Option<&[u8]>,
6607        sealed_transaction: Option<&[u8]>,
6608    ) -> anyhow::Result<ExecutionTransactionHashRow> {
6609        anyhow::ensure!(
6610            raw_transaction.is_some() != sealed_transaction.is_some(),
6611            "A signed transaction requires exactly one payload representation"
6612        );
6613        let chain_id_db = i32::try_from(chain_id)
6614            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
6615        let mut transaction =
6616            self.pool.begin().await.map_err(|e| {
6617                anyhow::anyhow!("Failed to start signed transaction persistence: {e}")
6618            })?;
6619
6620        if let Some(envelope) = sealed_transaction {
6621            let state_row = sqlx::query(
6622                "SELECT deployment_id, protocol_version, operation, active_key_id \
6623                 FROM execution_payload_state WHERE component = 'signed_transactions' FOR SHARE",
6624            )
6625            .fetch_optional(&mut *transaction)
6626            .await
6627            .context("failed to lock execution payload state for protected persistence")?
6628            .ok_or_else(|| anyhow::anyhow!("Execution payload protection is not active"))?;
6629            let state = execution_payload_state_from_row(&state_row)?;
6630            anyhow::ensure!(
6631                state.protocol_version == EXECUTION_PAYLOAD_PROTOCOL_VERSION
6632                    && state.operation == "ready",
6633                "Execution payload storage is not ready for protected persistence"
6634            );
6635            anyhow::ensure!(
6636                envelope_key_id(envelope)?.as_slice() == state.active_key_id.as_slice(),
6637                "Signed transaction envelope does not use the database active key"
6638            );
6639        } else {
6640            let marker = sqlx::query_scalar::<_, bool>(
6641                "SELECT EXISTS (SELECT 1 FROM execution_schema_version WHERE component = $1)",
6642            )
6643            .bind(EXECUTION_PAYLOAD_COMPONENT)
6644            .fetch_one(&mut *transaction)
6645            .await
6646            .context("failed to inspect execution payload marker")?;
6647            anyhow::ensure!(
6648                !marker,
6649                "Plaintext signed transaction persistence is disabled after payload activation"
6650            );
6651        }
6652        let current_status = sqlx::query_scalar::<_, String>(
6653            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
6654        )
6655        .bind(intent_id)
6656        .fetch_optional(&mut *transaction)
6657        .await
6658        .map_err(|e| anyhow::anyhow!("Failed to lock execution intent {intent_id}: {e}"))?
6659        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
6660        anyhow::ensure!(
6661            current_status == TransactionStatus::Prepared.as_str()
6662                || current_status == TransactionStatus::Signed.as_str(),
6663            "Execution intent {intent_id} is {current_status}, not prepared for signing"
6664        );
6665
6666        let row = sqlx::query_as::<_, ExecutionTransactionHashRow>(
6667            "
6668            INSERT INTO execution_transaction_hash (
6669                intent_id, chain_id, transaction_hash, payload_expected,
6670                raw_transaction, sealed_transaction, status
6671            )
6672            SELECT id, chain_id, $3, TRUE, $4, $5, 'signed'
6673            FROM execution_intent
6674            WHERE id = $1 AND chain_id = $2 AND nonce IS NOT NULL
6675            ON CONFLICT (chain_id, transaction_hash) DO UPDATE
6676            SET transaction_hash = EXCLUDED.transaction_hash
6677            WHERE execution_transaction_hash.intent_id = EXCLUDED.intent_id
6678              AND execution_transaction_hash.payload_expected
6679              AND (
6680                  (
6681                      EXCLUDED.raw_transaction IS NOT NULL
6682                      AND execution_transaction_hash.raw_transaction = EXCLUDED.raw_transaction
6683                      AND execution_transaction_hash.sealed_transaction IS NULL
6684                  )
6685                  OR (
6686                      EXCLUDED.sealed_transaction IS NOT NULL
6687                      AND execution_transaction_hash.raw_transaction IS NULL
6688                      AND execution_transaction_hash.sealed_transaction IS NOT NULL
6689                  )
6690              )
6691            RETURNING
6692                id, intent_id, chain_id, transaction_hash, payload_expected,
6693                raw_transaction, sealed_transaction, status,
6694                block_number, block_hash, receipt_success, gas_used,
6695                effective_gas_price, current
6696            ",
6697        )
6698        .bind(intent_id)
6699        .bind(chain_id_db)
6700        .bind(transaction_hash)
6701        .bind(raw_transaction)
6702        .bind(sealed_transaction)
6703        .fetch_optional(&mut *transaction)
6704        .await
6705        .map_err(|e| {
6706            anyhow::anyhow!("Failed to persist signed transaction {transaction_hash}: {e}")
6707        })?
6708        .ok_or_else(|| {
6709            anyhow::anyhow!(
6710                "Signed transaction {transaction_hash} conflicts with its persisted identity"
6711            )
6712        })?;
6713
6714        sqlx::query(
6715            "UPDATE execution_intent SET status = 'signed', updated_at = NOW() WHERE id = $1",
6716        )
6717        .bind(intent_id)
6718        .execute(&mut *transaction)
6719        .await
6720        .map_err(|e| anyhow::anyhow!("Failed to mark execution intent signed: {e}"))?;
6721        sqlx::query(
6722            "
6723            INSERT INTO execution_transaction_transition (
6724                intent_id, transaction_hash_id, transition_key, from_status, to_status
6725            ) VALUES ($1, $2, $3, $4, 'signed')
6726            ON CONFLICT (intent_id, transition_key) DO NOTHING
6727            ",
6728        )
6729        .bind(intent_id)
6730        .bind(row.id)
6731        .bind(format!("signed:{transaction_hash}"))
6732        .bind(current_status)
6733        .execute(&mut *transaction)
6734        .await
6735        .map_err(|e| anyhow::anyhow!("Failed to record signed transaction transition: {e}"))?;
6736
6737        transaction
6738            .commit()
6739            .await
6740            .map_err(|e| anyhow::anyhow!("Failed to commit signed transaction: {e}"))?;
6741        Ok(row)
6742    }
6743
6744    /// Records an idempotent transaction observation and advances its intent state.
6745    ///
6746    /// # Errors
6747    ///
6748    /// Returns an error if the transition is invalid, the hash is unknown, or persistence fails.
6749    #[expect(
6750        clippy::too_many_arguments,
6751        reason = "the parameters are the canonical receipt observation persisted atomically"
6752    )]
6753    pub async fn record_execution_status(
6754        &self,
6755        intent_id: i64,
6756        transaction_hash: &str,
6757        status: TransactionStatus,
6758        block_number: Option<u64>,
6759        block_hash: Option<&str>,
6760        receipt_success: Option<bool>,
6761        gas_used: Option<u64>,
6762        effective_gas_price: Option<&str>,
6763    ) -> anyhow::Result<()> {
6764        let block_number_db = block_number
6765            .map(i64::try_from)
6766            .transpose()
6767            .with_context(|| {
6768                format!(
6769                    "Execution block number {} exceeds PostgreSQL BIGINT",
6770                    block_number.unwrap_or_default()
6771                )
6772            })?;
6773        let gas_used_db = gas_used.map(i64::try_from).transpose().with_context(|| {
6774            format!(
6775                "Execution gas used {} exceeds PostgreSQL BIGINT",
6776                gas_used.unwrap_or_default()
6777            )
6778        })?;
6779        let mut transaction = self
6780            .pool
6781            .begin()
6782            .await
6783            .map_err(|e| anyhow::anyhow!("Failed to start execution status transition: {e}"))?;
6784        let (current_status, fill_emitted, terminal_emitted) =
6785            sqlx::query_as::<_, (String, bool, bool)>(
6786            "SELECT status, fill_emitted, terminal_emitted FROM execution_intent WHERE id = $1 FOR UPDATE",
6787        )
6788        .bind(intent_id)
6789        .fetch_optional(&mut *transaction)
6790        .await
6791        .map_err(|e| anyhow::anyhow!("Failed to lock execution intent {intent_id}: {e}"))?
6792        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
6793        anyhow::ensure!(
6794            execution_transition_allowed(&current_status, status),
6795            "Invalid execution transition for intent {intent_id}: {current_status} -> {}",
6796            status.as_str()
6797        );
6798
6799        let active = match status {
6800            TransactionStatus::Finalized | TransactionStatus::Reverted => {
6801                !fill_emitted && !terminal_emitted
6802            }
6803            TransactionStatus::Recoverable => false,
6804            _ => true,
6805        };
6806        let hash_result = sqlx::query(
6807            "
6808            UPDATE execution_transaction_hash
6809            SET status = $3,
6810                block_number = COALESCE($4, block_number),
6811                block_hash = COALESCE($5, block_hash),
6812                receipt_success = COALESCE($6, receipt_success),
6813                gas_used = COALESCE($7, gas_used),
6814                effective_gas_price = COALESCE($8, effective_gas_price),
6815                updated_at = NOW()
6816            WHERE intent_id = $1 AND transaction_hash = $2
6817            ",
6818        )
6819        .bind(intent_id)
6820        .bind(transaction_hash)
6821        .bind(status.as_str())
6822        .bind(block_number_db)
6823        .bind(block_hash)
6824        .bind(receipt_success)
6825        .bind(gas_used_db)
6826        .bind(effective_gas_price)
6827        .execute(&mut *transaction)
6828        .await
6829        .map_err(|e| anyhow::anyhow!("Failed to update execution hash {transaction_hash}: {e}"))?;
6830        anyhow::ensure!(
6831            hash_result.rows_affected() == 1,
6832            "Execution transaction hash {transaction_hash} was not found for intent {intent_id}"
6833        );
6834
6835        sqlx::query(
6836            "
6837            UPDATE execution_intent
6838            SET status = $2, active = $3, updated_at = NOW()
6839            WHERE id = $1
6840            ",
6841        )
6842        .bind(intent_id)
6843        .bind(status.as_str())
6844        .bind(active)
6845        .execute(&mut *transaction)
6846        .await
6847        .map_err(|e| anyhow::anyhow!("Failed to update execution intent {intent_id}: {e}"))?;
6848
6849        let transition_key = format!(
6850            "{}:{transaction_hash}:{}:{}",
6851            status.as_str(),
6852            block_number.map_or_else(|| "none".to_string(), |value| value.to_string()),
6853            block_hash.unwrap_or("none")
6854        );
6855        sqlx::query(
6856            "
6857            INSERT INTO execution_transaction_transition (
6858                intent_id, transaction_hash_id, transition_key, from_status, to_status,
6859                block_number, block_hash
6860            )
6861            SELECT $1, id, $3, $4, $5, $6, $7
6862            FROM execution_transaction_hash
6863            WHERE intent_id = $1 AND transaction_hash = $2
6864            ON CONFLICT (intent_id, transition_key) DO NOTHING
6865            ",
6866        )
6867        .bind(intent_id)
6868        .bind(transaction_hash)
6869        .bind(transition_key)
6870        .bind(current_status)
6871        .bind(status.as_str())
6872        .bind(block_number_db)
6873        .bind(block_hash)
6874        .execute(&mut *transaction)
6875        .await
6876        .map_err(|e| anyhow::anyhow!("Failed to record execution transition: {e}"))?;
6877
6878        transaction
6879            .commit()
6880            .await
6881            .map_err(|e| anyhow::anyhow!("Failed to commit execution transition: {e}"))?;
6882        Ok(())
6883    }
6884
6885    /// Records verified final consumption and advances the canonical nonce in one transaction.
6886    ///
6887    /// # Errors
6888    ///
6889    /// Returns an error if the receipt transition, nonce ledger, manifest identity, or evidence
6890    /// is inconsistent, or if persistence fails.
6891    pub(crate) async fn record_execution_finality_verified(
6892        &self,
6893        finality: &ExecutionFinalityTransition<'_>,
6894    ) -> anyhow::Result<()> {
6895        anyhow::ensure!(
6896            matches!(
6897                finality.status,
6898                TransactionStatus::Finalized | TransactionStatus::Reverted
6899            ),
6900            "Verified finality requires a finalized or reverted status"
6901        );
6902        anyhow::ensure!(
6903            !finality.decisions.is_empty(),
6904            "Verified finality requires decision evidence"
6905        );
6906        anyhow::ensure!(
6907            !finality.finalized_headers.is_empty()
6908                && finality.finalized_headers.windows(2).all(|headers| {
6909                    headers[1].number == headers[0].number.saturating_add(1)
6910                        && headers[1].parent_hash == headers[0].hash
6911                })
6912                && finality
6913                    .finalized_headers
6914                    .last()
6915                    .is_some_and(|header| header.number >= finality.block_number),
6916            "Verified finality headers must form a continuous chain through the inclusion height"
6917        );
6918        let chain_id = i32::try_from(finality.chain_id)
6919            .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
6920        let nonce =
6921            i64::try_from(finality.nonce).context("Execution nonce exceeds PostgreSQL BIGINT")?;
6922        let next_nonce = nonce
6923            .checked_add(1)
6924            .ok_or_else(|| anyhow::anyhow!("Canonical nonce overflow"))?;
6925        let block_number = i64::try_from(finality.block_number)
6926            .context("Finality block exceeds PostgreSQL BIGINT")?;
6927        let gas_used = i64::try_from(finality.gas_used)
6928            .context("Finality gas used exceeds PostgreSQL BIGINT")?;
6929        let mut transaction =
6930            self.pool.begin().await.map_err(|e| {
6931                anyhow::anyhow!("Failed to start verified finality transition: {e}")
6932            })?;
6933        let (manifest_version, manifest_digest, stored_nonce, revision) =
6934            sqlx::query_as::<_, (String, String, i64, i64)>(
6935                "
6936                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
6937                FROM execution_verification_nonce
6938                WHERE chain_id = $1 AND wallet_address = $2
6939                FOR UPDATE
6940                ",
6941            )
6942            .bind(chain_id)
6943            .bind(finality.wallet_address)
6944            .fetch_optional(&mut *transaction)
6945            .await
6946            .map_err(|e| anyhow::anyhow!("Failed to lock finality nonce ledger: {e}"))?
6947            .ok_or_else(|| anyhow::anyhow!("Canonical nonce ledger is not initialized"))?;
6948        anyhow::ensure!(
6949            manifest_version == finality.manifest_version
6950                && manifest_digest == finality.manifest_digest,
6951            "Verified finality manifest identity changed"
6952        );
6953        anyhow::ensure!(
6954            stored_nonce == nonce,
6955            "Finalized nonce {} does not match canonical nonce {stored_nonce}",
6956            finality.nonce
6957        );
6958        let stored_tip = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
6959            "
6960            SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
6961            FROM execution_verified_finalized_header
6962            WHERE chain_id = $1 AND wallet_address = $2
6963            ORDER BY number DESC
6964            LIMIT 1
6965            ",
6966        )
6967        .bind(chain_id)
6968        .bind(finality.wallet_address)
6969        .fetch_one(&mut *transaction)
6970        .await
6971        .map_err(|e| anyhow::anyhow!("Failed to lock finalized header tip: {e}"))?;
6972        let first_header = &finality.finalized_headers[0];
6973        anyhow::ensure!(
6974            stored_tip
6975                == (
6976                    i64::try_from(first_header.number)
6977                        .context("Verified finalized height exceeds PostgreSQL BIGINT")?,
6978                    first_header.hash.clone(),
6979                    first_header.parent_hash.clone(),
6980                    i64::try_from(first_header.timestamp)
6981                        .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?,
6982                    first_header.base_fee_per_gas.map(|value| value.to_string()),
6983                    finality.manifest_digest.to_string(),
6984                ),
6985            "Verified finality extension does not start at the durable tip"
6986        );
6987
6988        for header in finality.finalized_headers.iter().skip(1) {
6989            let number = i64::try_from(header.number)
6990                .context("Verified finalized height exceeds PostgreSQL BIGINT")?;
6991            let timestamp = i64::try_from(header.timestamp)
6992                .context("Verified finalized timestamp exceeds PostgreSQL BIGINT")?;
6993            let base_fee = header.base_fee_per_gas.map(|value| value.to_string());
6994            sqlx::query(
6995                "
6996                INSERT INTO execution_verified_finalized_header (
6997                    chain_id, wallet_address, number, hash, parent_hash, timestamp,
6998                    base_fee_per_gas, manifest_digest
6999                )
7000                VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
7001                ON CONFLICT (chain_id, wallet_address, number) DO NOTHING
7002                ",
7003            )
7004            .bind(chain_id)
7005            .bind(finality.wallet_address)
7006            .bind(number)
7007            .bind(&header.hash)
7008            .bind(&header.parent_hash)
7009            .bind(timestamp)
7010            .bind(&base_fee)
7011            .bind(finality.manifest_digest)
7012            .execute(&mut *transaction)
7013            .await
7014            .map_err(|e| anyhow::anyhow!("Failed to extend finalized header ledger: {e}"))?;
7015            let stored = sqlx::query_as::<_, (String, String, i64, Option<String>, String)>(
7016                "
7017                SELECT hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
7018                FROM execution_verified_finalized_header
7019                WHERE chain_id = $1 AND wallet_address = $2 AND number = $3
7020                ",
7021            )
7022            .bind(chain_id)
7023            .bind(finality.wallet_address)
7024            .bind(number)
7025            .fetch_one(&mut *transaction)
7026            .await
7027            .map_err(|e| anyhow::anyhow!("Failed to validate finalized header ledger: {e}"))?;
7028            anyhow::ensure!(
7029                stored
7030                    == (
7031                        header.hash.clone(),
7032                        header.parent_hash.clone(),
7033                        timestamp,
7034                        base_fee,
7035                        finality.manifest_digest.to_string(),
7036                    ),
7037                "Finalized header ledger conflicts at height {}",
7038                header.number
7039            );
7040        }
7041
7042        let (current_status, intent_nonce, fill_emitted, terminal_emitted) =
7043            sqlx::query_as::<_, (String, Option<i64>, bool, bool)>(
7044                "
7045                SELECT status, nonce, fill_emitted, terminal_emitted
7046                FROM execution_intent
7047                WHERE id = $1 AND chain_id = $2 AND wallet_address = $3 AND active
7048                FOR UPDATE
7049                ",
7050            )
7051            .bind(finality.intent_id)
7052            .bind(chain_id)
7053            .bind(finality.wallet_address)
7054            .fetch_optional(&mut *transaction)
7055            .await
7056            .map_err(|e| anyhow::anyhow!("Failed to lock intent for verified finality: {e}"))?
7057            .ok_or_else(|| anyhow::anyhow!("Active finality intent was not found"))?;
7058        anyhow::ensure!(
7059            intent_nonce == Some(nonce)
7060                && execution_transition_allowed(&current_status, finality.status),
7061            "Intent cannot make the verified finality transition"
7062        );
7063
7064        for (index, decision) in finality.decisions.iter().enumerate() {
7065            let height_start = decision
7066                .height_start
7067                .map(i64::try_from)
7068                .transpose()
7069                .context("Verification height exceeds PostgreSQL BIGINT")?;
7070            let height_end = decision
7071                .height_end
7072                .map(i64::try_from)
7073                .transpose()
7074                .context("Verification height exceeds PostgreSQL BIGINT")?;
7075            let transition_key = format!(
7076                "finality:{}:{}:{index}",
7077                finality.intent_id, decision.read_class
7078            );
7079            sqlx::query(
7080                "
7081                INSERT INTO execution_verification_decision (
7082                    intent_id, nonce, decision_class, read_class, height_start, height_end,
7083                    manifest_version, manifest_digest, provider_ids, operator_ids,
7084                    failure_domain_ids, response_class, normalized_value_digest,
7085                    nonce_revision, outcome, transition_key
7086                )
7087                VALUES (
7088                    $1, $2, 'finality', $3, $4, $5, $6, $7, $8, $9, $10,
7089                    'all_valid', $11, $12, 'verified', $13
7090                )
7091                ",
7092            )
7093            .bind(finality.intent_id)
7094            .bind(nonce)
7095            .bind(decision.read_class)
7096            .bind(height_start)
7097            .bind(height_end)
7098            .bind(finality.manifest_version)
7099            .bind(finality.manifest_digest)
7100            .bind(finality.provider_ids)
7101            .bind(finality.operator_ids)
7102            .bind(finality.failure_domain_ids)
7103            .bind(&decision.normalized_value_digest)
7104            .bind(revision)
7105            .bind(transition_key)
7106            .execute(&mut *transaction)
7107            .await
7108            .map_err(|e| anyhow::anyhow!("Failed to persist finality verification: {e}"))?;
7109        }
7110
7111        let hash_result = sqlx::query(
7112            "
7113            UPDATE execution_transaction_hash
7114            SET status = $3, block_number = $4, block_hash = $5,
7115                receipt_success = $6, gas_used = $7, effective_gas_price = $8,
7116                updated_at = NOW()
7117            WHERE intent_id = $1 AND transaction_hash = $2
7118            ",
7119        )
7120        .bind(finality.intent_id)
7121        .bind(finality.transaction_hash)
7122        .bind(finality.status.as_str())
7123        .bind(block_number)
7124        .bind(finality.block_hash)
7125        .bind(finality.receipt_success)
7126        .bind(gas_used)
7127        .bind(finality.effective_gas_price)
7128        .execute(&mut *transaction)
7129        .await
7130        .map_err(|e| anyhow::anyhow!("Failed to record verified finality receipt: {e}"))?;
7131        anyhow::ensure!(
7132            hash_result.rows_affected() == 1,
7133            "Finality transaction hash was not found"
7134        );
7135
7136        let active = !fill_emitted && !terminal_emitted;
7137        sqlx::query(
7138            "UPDATE execution_intent SET status = $2, active = $3, updated_at = NOW() WHERE id = $1",
7139        )
7140        .bind(finality.intent_id)
7141        .bind(finality.status.as_str())
7142        .bind(active)
7143        .execute(&mut *transaction)
7144        .await
7145        .map_err(|e| anyhow::anyhow!("Failed to record verified finality intent: {e}"))?;
7146
7147        let transition_key = format!(
7148            "{}:{}:{}:{}",
7149            finality.status.as_str(),
7150            finality.transaction_hash,
7151            finality.block_number,
7152            finality.block_hash
7153        );
7154        sqlx::query(
7155            "
7156            INSERT INTO execution_transaction_transition (
7157                intent_id, transaction_hash_id, transition_key, from_status, to_status,
7158                block_number, block_hash
7159            )
7160            SELECT $1, id, $3, $4, $5, $6, $7
7161            FROM execution_transaction_hash
7162            WHERE intent_id = $1 AND transaction_hash = $2
7163            ",
7164        )
7165        .bind(finality.intent_id)
7166        .bind(finality.transaction_hash)
7167        .bind(transition_key)
7168        .bind(current_status)
7169        .bind(finality.status.as_str())
7170        .bind(block_number)
7171        .bind(finality.block_hash)
7172        .execute(&mut *transaction)
7173        .await
7174        .map_err(|e| anyhow::anyhow!("Failed to record verified finality transition: {e}"))?;
7175
7176        let nonce_result = sqlx::query(
7177            "
7178            UPDATE execution_verification_nonce
7179            SET next_canonical_nonce = $3, revision = revision + 1, updated_at = NOW()
7180            WHERE chain_id = $1 AND wallet_address = $2
7181              AND next_canonical_nonce = $4 AND revision = $5
7182            ",
7183        )
7184        .bind(chain_id)
7185        .bind(finality.wallet_address)
7186        .bind(next_nonce)
7187        .bind(nonce)
7188        .bind(revision)
7189        .execute(&mut *transaction)
7190        .await
7191        .map_err(|e| anyhow::anyhow!("Failed to advance canonical nonce ledger: {e}"))?;
7192        anyhow::ensure!(
7193            nonce_result.rows_affected() == 1,
7194            "Canonical nonce ledger changed during finality transition"
7195        );
7196        transaction
7197            .commit()
7198            .await
7199            .map_err(|e| anyhow::anyhow!("Failed to commit verified finality transition: {e}"))?;
7200        Ok(())
7201    }
7202
7203    /// Loads one durable execution intent by ID.
7204    pub(crate) async fn get_execution_intent(
7205        &self,
7206        intent_id: i64,
7207    ) -> anyhow::Result<ExecutionIntentRow> {
7208        sqlx::query_as::<_, ExecutionIntentRow>(
7209            "
7210            SELECT
7211                id, schema_version, chain_id, wallet_address, nonce, purpose, status,
7212                client_order_id, trader_id, strategy_id, account_id, instrument_id,
7213                pool_address, transaction_to, transaction_input, transaction_value,
7214                amount_in, created_block, acknowledgement_emitted, fill_emitted,
7215                terminal_emitted, active
7216            FROM execution_intent
7217            WHERE id = $1
7218            ",
7219        )
7220        .bind(intent_id)
7221        .fetch_optional(&self.pool)
7222        .await
7223        .context("failed to load execution intent")?
7224        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))
7225    }
7226
7227    /// Loads the active intent owned by a signer after any concurrent reservation completes.
7228    ///
7229    /// # Errors
7230    ///
7231    /// Returns an error if the query fails.
7232    pub async fn get_active_execution_intent(
7233        &self,
7234        chain_id: u32,
7235        wallet_address: &str,
7236    ) -> anyhow::Result<Option<ExecutionIntentRow>> {
7237        let chain_id_db = i32::try_from(chain_id)
7238            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
7239        let mut transaction = self
7240            .pool
7241            .begin_with("BEGIN ISOLATION LEVEL READ COMMITTED")
7242            .await
7243            .context("failed to start active execution intent reconciliation")?;
7244        Self::lock_execution_signer(&mut transaction, chain_id, wallet_address).await?;
7245        let intent = sqlx::query_as::<_, ExecutionIntentRow>(
7246            "
7247            SELECT
7248                id, schema_version, chain_id, wallet_address, nonce, purpose, status,
7249                client_order_id, trader_id, strategy_id, account_id, instrument_id,
7250                pool_address, transaction_to, transaction_input, transaction_value,
7251                amount_in, created_block, acknowledgement_emitted, fill_emitted,
7252                terminal_emitted, active
7253            FROM execution_intent
7254            WHERE chain_id = $1 AND wallet_address = $2 AND active
7255            ",
7256        )
7257        .bind(chain_id_db)
7258        .bind(wallet_address)
7259        .fetch_optional(&mut *transaction)
7260        .await
7261        .context("failed to load active execution intent")?;
7262        transaction
7263            .commit()
7264            .await
7265            .context("failed to complete active execution intent reconciliation")?;
7266        Ok(intent)
7267    }
7268
7269    async fn lock_execution_signer(
7270        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
7271        chain_id: u32,
7272        wallet_address: &str,
7273    ) -> anyhow::Result<()> {
7274        let lock = PgAdvisoryLock::new(format!(
7275            "nautilus:blockchain:execution:{chain_id}:{}",
7276            wallet_address.to_ascii_lowercase()
7277        ));
7278        let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
7279            unreachable!("string advisory locks use the 64-bit key space");
7280        };
7281
7282        // Transaction-scoped release makes row presence or absence authoritative to the next
7283        // holder after an ambiguous reservation commit.
7284        sqlx::query("SELECT pg_advisory_xact_lock($1)")
7285            .bind(*lock_key)
7286            .execute(&mut **transaction)
7287            .await
7288            .context("failed to acquire execution signer reservation fence")?;
7289        Ok(())
7290    }
7291
7292    /// Reports whether a released legacy intent still retains a broadcastable signature.
7293    ///
7294    /// # Errors
7295    ///
7296    /// Returns an error if the query fails.
7297    pub async fn has_recoverable_signed_execution(
7298        &self,
7299        chain_id: u32,
7300        wallet_address: &str,
7301    ) -> anyhow::Result<bool> {
7302        let chain_id_db = i32::try_from(chain_id)
7303            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
7304        sqlx::query_scalar::<_, bool>(
7305            "
7306            SELECT EXISTS (
7307                SELECT 1
7308                FROM execution_intent AS intent
7309                JOIN execution_transaction_hash AS hash ON hash.intent_id = intent.id
7310                WHERE intent.chain_id = $1
7311                  AND intent.wallet_address = $2
7312                  AND intent.status = 'recoverable'
7313                  AND (hash.raw_transaction IS NOT NULL OR hash.sealed_transaction IS NOT NULL)
7314            )
7315            ",
7316        )
7317        .bind(chain_id_db)
7318        .bind(wallet_address)
7319        .fetch_one(&self.pool)
7320        .await
7321        .map_err(|e| anyhow::anyhow!("Failed to inspect recoverable signed executions: {e}"))
7322    }
7323
7324    /// Loads all transaction hashes for an intent in insertion order.
7325    ///
7326    /// # Errors
7327    ///
7328    /// Returns an error if the query fails.
7329    pub async fn get_execution_transaction_hashes(
7330        &self,
7331        intent_id: i64,
7332    ) -> anyhow::Result<Vec<ExecutionTransactionHashRow>> {
7333        sqlx::query_as::<_, ExecutionTransactionHashRow>(
7334            "
7335            SELECT
7336                id, intent_id, chain_id, transaction_hash, payload_expected,
7337                raw_transaction, sealed_transaction, status,
7338                block_number, block_hash, receipt_success, gas_used,
7339                effective_gas_price, current
7340            FROM execution_transaction_hash
7341            WHERE intent_id = $1
7342            ORDER BY id
7343            ",
7344        )
7345        .bind(intent_id)
7346        .fetch_all(&self.pool)
7347        .await
7348        .map_err(|e| anyhow::anyhow!("Failed to load execution transaction hashes: {e}"))
7349    }
7350
7351    /// Marks one order event as emitted after dispatch.
7352    ///
7353    /// # Errors
7354    ///
7355    /// Returns an error if the event kind is unknown, the intent is absent, the opposing
7356    /// terminal marker is already set, or persistence fails.
7357    pub async fn mark_execution_event_emitted(
7358        &self,
7359        intent_id: i64,
7360        event: &str,
7361    ) -> anyhow::Result<()> {
7362        let statement = match event {
7363            "acknowledgement" => {
7364                "UPDATE execution_intent SET acknowledgement_emitted = TRUE, updated_at = NOW() WHERE id = $1"
7365            }
7366            "fill" => {
7367                "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"
7368            }
7369            "terminal" => {
7370                "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"
7371            }
7372            _ => anyhow::bail!("Unknown execution event marker {event}"),
7373        };
7374        let result = sqlx::query(statement)
7375            .bind(intent_id)
7376            .execute(&self.pool)
7377            .await
7378            .map_err(|e| anyhow::anyhow!("Failed to mark execution {event} emitted: {e}"))?;
7379        anyhow::ensure!(
7380            result.rows_affected() == 1,
7381            "Execution intent {intent_id} cannot mark {event} emitted"
7382        );
7383        Ok(())
7384    }
7385
7386    /// Updates the status of a persisted execution transaction record.
7387    ///
7388    /// # Errors
7389    ///
7390    /// Returns an error if the database operation fails.
7391    pub async fn update_execution_transaction_status(
7392        &self,
7393        chain_id: u32,
7394        transaction_hash: &str,
7395        status: &str,
7396    ) -> anyhow::Result<()> {
7397        let result = sqlx::query(
7398            "
7399            UPDATE execution_transaction
7400            SET status = $3
7401            WHERE chain_id = $1 AND transaction_hash = $2
7402        ",
7403        )
7404        .bind(chain_id as i32)
7405        .bind(transaction_hash)
7406        .bind(status)
7407        .execute(&self.pool)
7408        .await
7409        .map_err(|e| anyhow::anyhow!("Failed to update execution_transaction table: {e}"))?;
7410
7411        anyhow::ensure!(
7412            result.rows_affected() == 1,
7413            "Execution transaction {transaction_hash} was not found for status update"
7414        );
7415        Ok(())
7416    }
7417
7418    /// Loads an execution transaction record by chain ID and transaction hash.
7419    ///
7420    /// # Errors
7421    ///
7422    /// Returns an error if the database operation fails.
7423    pub async fn get_execution_transaction(
7424        &self,
7425        chain_id: u32,
7426        transaction_hash: &str,
7427    ) -> anyhow::Result<Option<ExecutionTransactionRow>> {
7428        let chain_id_db = i32::try_from(chain_id)
7429            .with_context(|| format!("Chain ID {chain_id} exceeds PostgreSQL INTEGER"))?;
7430        sqlx::query_as::<_, ExecutionTransactionRow>(
7431            "
7432            SELECT wallet_address, nonce, transaction_hash, purpose, status, client_order_id
7433            FROM (
7434                SELECT
7435                    intent.wallet_address,
7436                    intent.nonce,
7437                    hash.transaction_hash,
7438                    intent.purpose,
7439                    intent.status,
7440                    intent.client_order_id,
7441                    0 AS source_priority
7442                FROM execution_transaction_hash AS hash
7443                JOIN execution_intent AS intent ON intent.id = hash.intent_id
7444                WHERE hash.chain_id = $1 AND hash.transaction_hash = $2
7445                UNION ALL
7446                SELECT
7447                    wallet_address,
7448                    nonce,
7449                    transaction_hash,
7450                    purpose,
7451                    status,
7452                    client_order_id,
7453                    1 AS source_priority
7454                FROM execution_transaction
7455                WHERE chain_id = $1 AND transaction_hash = $2
7456            ) AS record
7457            ORDER BY source_priority
7458            LIMIT 1
7459        ",
7460        )
7461        .bind(chain_id_db)
7462        .bind(transaction_hash)
7463        .fetch_optional(&self.pool)
7464        .await
7465        .map_err(|e| anyhow::anyhow!("Failed to load from execution_transaction table: {e}"))
7466    }
7467}
7468
7469fn execution_payload_state_from_row(
7470    row: &sqlx::postgres::PgRow,
7471) -> anyhow::Result<ExecutionPayloadState> {
7472    Ok(ExecutionPayloadState {
7473        deployment_id: row.try_get("deployment_id")?,
7474        protocol_version: row.try_get("protocol_version")?,
7475        operation: row.try_get("operation")?,
7476        active_key_id: row.try_get("active_key_id")?,
7477    })
7478}
7479
7480fn validate_execution_payload_state(
7481    state: &ExecutionPayloadState,
7482    keys: &PayloadKeySet,
7483) -> anyhow::Result<()> {
7484    anyhow::ensure!(
7485        state.protocol_version == EXECUTION_PAYLOAD_PROTOCOL_VERSION,
7486        "Execution payload protocol version {} is not supported",
7487        state.protocol_version
7488    );
7489    anyhow::ensure!(
7490        state.deployment_id == keys.deployment_id(),
7491        "Execution payload deployment ID does not match this database"
7492    );
7493    anyhow::ensure!(
7494        state.active_key_id.as_slice() == keys.active_key_id(),
7495        "Configured active payload key does not match the database active key"
7496    );
7497    Ok(())
7498}
7499
7500async fn lock_execution_payload_operation(
7501    transaction: &mut Transaction<'_, Postgres>,
7502) -> anyhow::Result<()> {
7503    let lock = PgAdvisoryLock::new("nautilus:blockchain:execution-payload");
7504    let PgAdvisoryLockKey::BigInt(lock_key) = lock.key() else {
7505        unreachable!("string advisory locks use the 64-bit key space");
7506    };
7507    sqlx::query("SELECT pg_advisory_xact_lock($1)")
7508        .bind(*lock_key)
7509        .execute(&mut **transaction)
7510        .await
7511        .context("failed to acquire execution payload operation fence")?;
7512    Ok(())
7513}
7514
7515async fn reserve_execution_payload_seal(
7516    transaction: &mut Transaction<'_, Postgres>,
7517    key_id: &[u8; 32],
7518) -> anyhow::Result<()> {
7519    let seals = sqlx::query_scalar::<_, i64>(
7520        "INSERT INTO execution_payload_key_state (key_id, seals) VALUES ($1, 1) \
7521         ON CONFLICT (key_id) DO UPDATE SET seals = execution_payload_key_state.seals + 1 \
7522         WHERE execution_payload_key_state.seals < $2 \
7523         RETURNING seals",
7524    )
7525    .bind(key_id.as_slice())
7526    .bind(EXECUTION_PAYLOAD_MAX_SEALS - 1)
7527    .fetch_optional(&mut **transaction)
7528    .await
7529    .context("failed to reserve execution payload nonce use")?;
7530    anyhow::ensure!(
7531        seals.is_some(),
7532        "Execution payload key reached its seal limit; rotate the active key before continuing"
7533    );
7534    Ok(())
7535}
7536
7537async fn validate_execution_payload_key_inventory(
7538    transaction: &mut Transaction<'_, Postgres>,
7539    keys: &PayloadKeySet,
7540) -> anyhow::Result<()> {
7541    let envelopes = sqlx::query_scalar::<_, Vec<u8>>(
7542        "SELECT DISTINCT substring(sealed_transaction FROM 1 FOR 33) \
7543         FROM execution_transaction_hash \
7544         WHERE sealed_transaction IS NOT NULL",
7545    )
7546    .fetch_all(&mut **transaction)
7547    .await
7548    .context("failed to inspect execution payload key inventory")?;
7549
7550    for header in envelopes {
7551        anyhow::ensure!(
7552            header.len() == 33,
7553            "Stored execution payload has a truncated envelope header"
7554        );
7555        let mut envelope = header;
7556        envelope.extend_from_slice(&[0; 12 + 16]);
7557        let key_id = envelope_key_id(&envelope)?;
7558        anyhow::ensure!(
7559            keys.contains_key(&key_id),
7560            "Stored execution payload requires unavailable key {}",
7561            alloy::hex::encode(key_id)
7562        );
7563    }
7564    Ok(())
7565}
7566
7567async fn load_execution_intent(
7568    transaction: &mut Transaction<'_, Postgres>,
7569    intent_id: i64,
7570) -> anyhow::Result<ExecutionIntentRow> {
7571    sqlx::query_as::<_, ExecutionIntentRow>(
7572        "
7573        SELECT
7574            id, schema_version, chain_id, wallet_address, nonce, purpose, status,
7575            client_order_id, trader_id, strategy_id, account_id, instrument_id,
7576            pool_address, transaction_to, transaction_input, transaction_value,
7577            amount_in, created_block, acknowledgement_emitted, fill_emitted,
7578            terminal_emitted, active
7579        FROM execution_intent
7580        WHERE id = $1
7581        FOR SHARE
7582        ",
7583    )
7584    .bind(intent_id)
7585    .fetch_optional(&mut **transaction)
7586    .await
7587    .context("failed to load execution intent for payload authentication")?
7588    .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))
7589}
7590
7591fn execution_transition_allowed(current: &str, next: TransactionStatus) -> bool {
7592    if current == next.as_str() {
7593        return true;
7594    }
7595
7596    match current {
7597        "prepared" => matches!(
7598            next,
7599            TransactionStatus::Signed | TransactionStatus::Recoverable
7600        ),
7601        "signed" | "broadcast" => matches!(
7602            next,
7603            TransactionStatus::Broadcast
7604                | TransactionStatus::Included
7605                | TransactionStatus::Finalized
7606                | TransactionStatus::Reverted
7607                | TransactionStatus::Replaced
7608                | TransactionStatus::Dropped
7609                | TransactionStatus::Reorged
7610        ),
7611        "included" => matches!(
7612            next,
7613            TransactionStatus::Finalized
7614                | TransactionStatus::Reverted
7615                | TransactionStatus::Reorged
7616                | TransactionStatus::Replaced
7617                | TransactionStatus::Dropped
7618        ),
7619        "replaced" | "dropped" | "reorged" => matches!(
7620            next,
7621            TransactionStatus::Included
7622                | TransactionStatus::Finalized
7623                | TransactionStatus::Reverted
7624                | TransactionStatus::Replaced
7625                | TransactionStatus::Dropped
7626                | TransactionStatus::Reorged
7627        ),
7628        "finalized" | "reverted" | "recoverable" => false,
7629        _ => false,
7630    }
7631}
7632
7633#[cfg(test)]
7634pub(crate) mod tests {
7635    use std::time::Duration;
7636
7637    use anyhow::Context;
7638    use sqlx::postgres::{PgConnectOptions, PgPoolOptions};
7639
7640    use super::{BlockchainCacheDatabase, ExecutionVerifiedHeader};
7641    use crate::rpc::verification::VERIFICATION_SCHEMA_VERSION;
7642
7643    pub(crate) struct ExecutionVerificationResume {
7644        pub next_canonical_nonce: u64,
7645        pub revision: u64,
7646        pub finalized_headers: Vec<ExecutionVerifiedHeader>,
7647    }
7648
7649    pub(crate) async fn connect_test_database(
7650        pg_options: PgConnectOptions,
7651    ) -> anyhow::Result<BlockchainCacheDatabase> {
7652        let pool = PgPoolOptions::new()
7653            .max_connections(32)
7654            .min_connections(1)
7655            .acquire_timeout(Duration::from_secs(3))
7656            .connect_with(pg_options)
7657            .await?;
7658        Ok(BlockchainCacheDatabase { pool })
7659    }
7660
7661    impl BlockchainCacheDatabase {
7662        /// Loads the complete durable finalized-header ledger for tests
7663        pub(crate) async fn load_execution_verification_resume(
7664            &self,
7665            chain_id: u32,
7666            wallet_address: &str,
7667            manifest_version: &str,
7668            manifest_digest: &str,
7669        ) -> anyhow::Result<Option<ExecutionVerificationResume>> {
7670            let installed = sqlx::query_scalar::<_, i16>(
7671                "SELECT version FROM execution_schema_version \
7672                 WHERE component = 'evm_execution_verification'",
7673            )
7674            .fetch_optional(&self.pool)
7675            .await
7676            .context("failed to inspect execution verification schema")?;
7677            let Some(installed) = installed else {
7678                return Ok(None);
7679            };
7680            anyhow::ensure!(
7681                installed <= VERIFICATION_SCHEMA_VERSION,
7682                "Unsupported execution verification schema version {installed}"
7683            );
7684            let chain_id = i32::try_from(chain_id)
7685                .context("Verification chain ID exceeds PostgreSQL INTEGER")?;
7686            let current = sqlx::query_as::<_, (String, String, i64, i64)>(
7687                "
7688                SELECT manifest_version, manifest_digest, next_canonical_nonce, revision
7689                FROM execution_verification_nonce
7690                WHERE chain_id = $1 AND wallet_address = $2
7691                ",
7692            )
7693            .bind(chain_id)
7694            .bind(wallet_address)
7695            .fetch_optional(&self.pool)
7696            .await
7697            .context("failed to load execution verification nonce position")?;
7698            let Some((stored_version, stored_digest, nonce, revision)) = current else {
7699                return Ok(None);
7700            };
7701            anyhow::ensure!(
7702                stored_version == manifest_version && stored_digest == manifest_digest,
7703                "Execution verification manifest identity changed"
7704            );
7705            let rows = sqlx::query_as::<_, (i64, String, String, i64, Option<String>, String)>(
7706                "
7707                SELECT number, hash, parent_hash, timestamp, base_fee_per_gas, manifest_digest
7708                FROM execution_verified_finalized_header
7709                WHERE chain_id = $1 AND wallet_address = $2
7710                ORDER BY number
7711                ",
7712            )
7713            .bind(chain_id)
7714            .bind(wallet_address)
7715            .fetch_all(&self.pool)
7716            .await
7717            .context("failed to load verified finalized header ledger")?;
7718            let finalized_headers = rows
7719                .into_iter()
7720                .map(|(number, hash, parent_hash, timestamp, base_fee, digest)| {
7721                    anyhow::ensure!(
7722                        digest == manifest_digest,
7723                        "Finalized header manifest identity changed"
7724                    );
7725                    Ok(ExecutionVerifiedHeader {
7726                        number: u64::try_from(number)
7727                            .context("Finalized header number is negative")?,
7728                        hash,
7729                        parent_hash,
7730                        timestamp: u64::try_from(timestamp)
7731                            .context("Finalized header timestamp is negative")?,
7732                        base_fee_per_gas: base_fee
7733                            .map(|value| {
7734                                value.parse::<u128>().map_err(|_| {
7735                                    anyhow::anyhow!("Finalized header base fee is invalid")
7736                                })
7737                            })
7738                            .transpose()?,
7739                    })
7740                })
7741                .collect::<anyhow::Result<Vec<_>>>()?;
7742            anyhow::ensure!(
7743                !finalized_headers.is_empty()
7744                    && finalized_headers.windows(2).all(|headers| {
7745                        headers[1].number == headers[0].number.saturating_add(1)
7746                            && headers[1].parent_hash == headers[0].hash
7747                    }),
7748                "Verified finalized header ledger is not continuous"
7749            );
7750            Ok(Some(ExecutionVerificationResume {
7751                next_canonical_nonce: u64::try_from(nonce)
7752                    .context("Canonical nonce is negative")?,
7753                revision: u64::try_from(revision)
7754                    .context("Canonical nonce revision is negative")?,
7755                finalized_headers,
7756            }))
7757        }
7758    }
7759
7760    #[tokio::test]
7761    async fn connect_returns_err_for_unreachable_database() {
7762        // `connect` backs the Python `load_pool_snapshot` binding, so a connection
7763        // failure must surface as `Err` rather than panicking across the API boundary.
7764        let options = PgConnectOptions::new()
7765            .host("127.0.0.1")
7766            .port(1)
7767            .username("nautilus")
7768            .password("pass")
7769            .database("nautilus");
7770
7771        let result = BlockchainCacheDatabase::connect(options).await;
7772
7773        assert!(result.is_err());
7774    }
7775}