diff --git a/.github/workflows/build-images.yml b/.github/workflows/build-images.yml new file mode 100644 index 00000000..1652fa37 --- /dev/null +++ b/.github/workflows/build-images.yml @@ -0,0 +1,92 @@ +name: Build and Publish Images + +on: + push: + branches: [ main ] + tags: + - 'node-v*' + - 'sequencer-v*' + workflow_dispatch: + inputs: + build_node: + description: 'Build Node image' + required: false + default: true + type: boolean + build_sequencer: + description: 'Build Sequencer image' + required: false + default: true + type: boolean + +env: + REGISTRY: ghcr.io + +jobs: + build: + runs-on: ubuntu-latest + strategy: + matrix: + service: + - name: node + context: . + dockerfile: Dockerfile + image: ${{ github.repository_owner }}/facet-node + - name: sequencer + context: ./sequencer + dockerfile: Dockerfile + image: ${{ github.repository_owner }}/facet-sequencer + + steps: + - name: Determine build target + id: should_build + run: | + if [[ "${{ github.event_name }}" != "workflow_dispatch" ]]; then + echo "build=true" >> $GITHUB_OUTPUT + elif [[ "${{ matrix.service.name }}" == "node" && "${{ inputs.build_node }}" == "true" ]]; then + echo "build=true" >> $GITHUB_OUTPUT + elif [[ "${{ matrix.service.name }}" == "sequencer" && "${{ inputs.build_sequencer }}" == "true" ]]; then + echo "build=true" >> $GITHUB_OUTPUT + else + echo "build=false" >> $GITHUB_OUTPUT + fi + + - name: Checkout + if: steps.should_build.outputs.build == 'true' + uses: actions/checkout@v4 + + - name: Log in to GHCR + if: steps.should_build.outputs.build == 'true' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + if: steps.should_build.outputs.build == 'true' + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ matrix.service.image }} + tags: | + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} + type=ref,event=tag,enable=${{ matrix.service.name == 'node' && startsWith(github.ref, 'refs/tags/node-v') }} + type=ref,event=tag,enable=${{ matrix.service.name == 'sequencer' && startsWith(github.ref, 'refs/tags/sequencer-v') }} + type=sha + + - name: Set up Docker Buildx + if: steps.should_build.outputs.build == 'true' + uses: docker/setup-buildx-action@v3 + + - name: Build and push + if: steps.should_build.outputs.build == 'true' + uses: docker/build-push-action@v5 + with: + context: ${{ matrix.service.context }} + file: ${{ matrix.service.dockerfile }} + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ env.REGISTRY }}/0xfacet/${{ matrix.service.name == 'node' && 'facet-node' || 'facet-sequencer' }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/0xfacet/${{ matrix.service.name == 'node' && 'facet-node' || 'facet-sequencer' }}:buildcache,mode=max diff --git a/.github/workflows/build-sequencer.yml b/.github/workflows/build-sequencer.yml new file mode 100644 index 00000000..ed216fbf --- /dev/null +++ b/.github/workflows/build-sequencer.yml @@ -0,0 +1,59 @@ +name: Build Sequencer Image + +on: + workflow_dispatch: + inputs: + tag: + description: 'Image tag (default: branch name)' + required: false + type: string + +env: + REGISTRY: ghcr.io + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Determine tag + id: tag + run: | + if [[ -n "${{ inputs.tag }}" ]]; then + echo "tag=${{ inputs.tag }}" >> $GITHUB_OUTPUT + else + # Use branch name as tag, replace / with - + BRANCH=${GITHUB_REF#refs/heads/} + echo "tag=${BRANCH//\//-}" >> $GITHUB_OUTPUT + fi + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/0xfacet/facet-sequencer + tags: | + type=raw,value=${{ steps.tag.outputs.tag }} + type=sha + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: ./sequencer + file: ./sequencer/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} \ No newline at end of file diff --git a/.gitignore b/.gitignore index 97ad36c3..e00f1e52 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ # Ignore all environment files (except templates). /.env* !/.env*.erb +!/.env.node.example # Ignore all logfiles and tempfiles. /log/* @@ -34,3 +35,7 @@ /contracts/forge-artifacts/ /contracts/cache/ /contracts/broadcast/* + +# Node.js dependencies +node_modules/ +package-lock.json diff --git a/Gemfile b/Gemfile index 1eb7617b..2308a4b1 100644 --- a/Gemfile +++ b/Gemfile @@ -76,3 +76,5 @@ gem 'ostruct' gem "oj", "~> 3.16" gem "retriable", "~> 3.1" + +gem "colorize", "~> 1.1" diff --git a/Gemfile.lock b/Gemfile.lock index 3b0883b1..e550de14 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -114,6 +114,7 @@ GEM activesupport tzinfo coderay (1.1.3) + colorize (1.1.0) concurrent-ruby (1.3.3) connection_pool (2.4.1) crass (1.0.6) @@ -347,6 +348,7 @@ DEPENDENCIES capybara clipboard (~> 2.0) clockwork (~> 3.0) + colorize (~> 1.1) debug dotenv-rails (~> 3.1) eth! diff --git a/app/models/eth_blob.rb b/app/models/eth_blob.rb new file mode 100644 index 00000000..0793ff01 --- /dev/null +++ b/app/models/eth_blob.rb @@ -0,0 +1,38 @@ +# Represents an EIP-4844 blob associated with an L1 transaction +class EthBlob < T::Struct + const :tx_hash, Hash32 # L1 transaction hash that carried this blob + const :l1_tx_index, Integer # Transaction index in L1 block + const :blob_index, Integer # Index of blob within the transaction (0-based) + const :versioned_hash, Hash32 # KZG commitment versioned hash + const :data, T.nilable(ByteString) # Raw blob data (nil if not fetched/available) + const :l1_block_number, Integer # L1 block number for tracking + + sig { params( + tx_hash: T.any(String, Hash32), + l1_tx_index: Integer, + blob_index: Integer, + versioned_hash: T.any(String, Hash32), + l1_block_number: Integer, + data: T.nilable(T.any(String, ByteString)) + ).returns(EthBlob) } + def self.create(tx_hash:, l1_tx_index:, blob_index:, versioned_hash:, l1_block_number:, data: nil) + new( + tx_hash: tx_hash.is_a?(Hash32) ? tx_hash : Hash32.from_hex(tx_hash), + l1_tx_index: l1_tx_index, + blob_index: blob_index, + versioned_hash: versioned_hash.is_a?(Hash32) ? versioned_hash : Hash32.from_hex(versioned_hash), + l1_block_number: l1_block_number, + data: data.nil? ? nil : (data.is_a?(ByteString) ? data : ByteString.from_hex(data)) + ) + end + + sig { returns(T::Boolean) } + def has_data? + !data.nil? + end + + sig { returns(String) } + def unique_id + "#{tx_hash.to_hex}-#{blob_index}" + end +end \ No newline at end of file diff --git a/app/models/facet_batch_constants.rb b/app/models/facet_batch_constants.rb new file mode 100644 index 00000000..b3ccc467 --- /dev/null +++ b/app/models/facet_batch_constants.rb @@ -0,0 +1,42 @@ +# Constants for Facet Batch V2 protocol +module FacetBatchConstants + # Magic prefix ("unstoppable sequencing" ASCII -> hex) + MAGIC_PREFIX = ByteString.from_hex("0x756e73746f707061626c652073657175656e63696e67") + + # Protocol version + VERSION = 1 + + # Wire format header sizes (in bytes) + MAGIC_SIZE = MAGIC_PREFIX.to_bin.bytesize + CHAIN_ID_SIZE = 8 # uint64 + VERSION_SIZE = 1 # uint8 + ROLE_SIZE = 1 # uint8 + LENGTH_SIZE = 4 # uint32 + HEADER_SIZE = MAGIC_SIZE + CHAIN_ID_SIZE + VERSION_SIZE + ROLE_SIZE + LENGTH_SIZE # 36 bytes + SIGNATURE_SIZE = 65 # secp256k1: r(32) + s(32) + v(1) + + # Wire format offsets + MAGIC_OFFSET = 0 + CHAIN_ID_OFFSET = MAGIC_SIZE + VERSION_OFFSET = CHAIN_ID_OFFSET + CHAIN_ID_SIZE + ROLE_OFFSET = VERSION_OFFSET + VERSION_SIZE + LENGTH_OFFSET = ROLE_OFFSET + ROLE_SIZE + RLP_OFFSET = HEADER_SIZE + + # Size limits + MAX_BATCH_BYTES = Integer(ENV.fetch('MAX_BATCH_BYTES', 131_072)) # 128KB default + MAX_TXS_PER_BATCH = Integer(ENV.fetch('MAX_TXS_PER_BATCH', 1000)) + MAX_BATCHES_PER_PAYLOAD = Integer(ENV.fetch('MAX_BATCHES_PER_PAYLOAD', 10)) + + # Batch roles + module Role + PERMISSIONLESS = 0x00 # Anyone can post, no signature required (formerly FORCED) + PRIORITY = 0x01 # Requires authorized signature + end + + # Source types for tracking where batch came from + module Source + CALLDATA = 'calldata' + BLOB = 'blob' + end +end diff --git a/app/models/parsed_batch.rb b/app/models/parsed_batch.rb new file mode 100644 index 00000000..38deb06f --- /dev/null +++ b/app/models/parsed_batch.rb @@ -0,0 +1,53 @@ +# Represents a parsed and validated Facet batch +class ParsedBatch < T::Struct + extend T::Sig + + const :role, Integer # PERMISSIONLESS or PRIORITY + const :signer, T.nilable(Address20) # Signer address (nil if not verified or permissionless) + const :l1_tx_index, Integer # Transaction index in L1 block + const :source, String # Where batch came from (calldata/blob) + const :source_details, T::Hash[Symbol, T.untyped] # Additional source info (tx_hash, blob_index, etc.) + const :transactions, T::Array[ByteString] # Array of EIP-2718 typed transaction bytes + const :content_hash, Hash32 # Keccak256 of RLP_TX_LIST for deduplication + const :chain_id, Integer # Chain ID from batch header + + sig { returns(T::Boolean) } + def is_priority? + role == FacetBatchConstants::Role::PRIORITY + end + + sig { returns(T::Boolean) } + def is_permissionless? + role == FacetBatchConstants::Role::PERMISSIONLESS + end + + sig { returns(Integer) } + def transaction_count + transactions.length + end + + sig { returns(T::Boolean) } + def has_signature? + !signer.nil? + end + + sig { returns(String) } + def source_description + case source + when FacetBatchConstants::Source::CALLDATA + "calldata from tx #{source_details[:tx_hash]}" + when FacetBatchConstants::Source::BLOB + "blob #{source_details[:blob_index]} from tx #{source_details[:tx_hash]}" + else + source + end + end + + # Calculate total gas limit for all transactions in batch + sig { returns(Integer) } + def total_gas_limit + # This will be calculated when we parse the actual transaction objects + # For now, return a placeholder + transactions.length * 21000 # Minimum gas per tx + end +end \ No newline at end of file diff --git a/app/models/standard_l2_transaction.rb b/app/models/standard_l2_transaction.rb new file mode 100644 index 00000000..bb71e5e6 --- /dev/null +++ b/app/models/standard_l2_transaction.rb @@ -0,0 +1,275 @@ +# Represents a standard EIP-2718 typed transaction (EIP-1559, EIP-2930, or legacy) +# These are the transactions that come from batches and go into L2 blocks, +# as opposed to FacetTransaction which is the special V1 single transaction format (0x7D/0x7E) +class StandardL2Transaction < T::Struct + class DecodeError < StandardError; end + const :raw_bytes, ByteString + const :tx_hash, Hash32 + const :from_address, Address20 + const :to_address, T.nilable(Address20) + const :nonce, Integer + const :gas_limit, Integer + const :value, Integer + const :data, ByteString + const :tx_type, Integer # 0x00 (legacy), 0x01 (EIP-2930), 0x02 (EIP-1559) + + # EIP-1559 specific fields + const :max_fee_per_gas, T.nilable(Integer) + const :max_priority_fee_per_gas, T.nilable(Integer) + + # Legacy/EIP-2930 field + const :gas_price, T.nilable(Integer) + + # Block association - writable property so it can be set after creation + prop :facet_block, T.nilable(FacetBlock) + + # Return raw bytes for proposer - compatible with block building + def to_raw + raw_bytes + end + + # Return payload for Geth - standard transactions just return their raw bytes + def to_facet_payload + raw_bytes.to_hex + end + + # Parse raw transaction bytes into StandardL2Transaction + def self.from_raw_bytes(raw_bytes) + bytes = raw_bytes.is_a?(ByteString) ? raw_bytes : ByteString.from_bin(raw_bytes) + tx_hash = Hash32.from_bin(Eth::Util.keccak256(bytes.to_bin)) + + # Determine transaction type + first_byte = bytes.to_bin[0].ord + + case first_byte + when 0x02 + parse_eip1559_transaction(bytes, tx_hash) + when 0x01 + parse_eip2930_transaction(bytes, tx_hash) + else + # Legacy transaction (no type byte or invalid type) + parse_legacy_transaction(bytes, tx_hash) + end + rescue StandardError => e + raise DecodeError, "Failed to decode transaction: #{e.message}" + end + + private + + def self.parse_eip1559_transaction(raw_bytes, tx_hash) + # Skip type byte and decode RLP + rlp_data = raw_bytes.to_bin[1..-1] + decoded = Eth::Rlp.decode(rlp_data) + + # EIP-1559 format: + # [chain_id, nonce, max_priority_fee, max_fee, gas_limit, to, value, data, access_list, v, r, s] + + chain_id = deserialize_int(decoded[0]) + nonce = deserialize_int(decoded[1]) + max_priority_fee = deserialize_int(decoded[2]) + max_fee = deserialize_int(decoded[3]) + gas_limit = deserialize_int(decoded[4]) + to_address = decoded[5].empty? ? nil : Address20.from_bin(decoded[5]) + value = deserialize_int(decoded[6]) + data = ByteString.from_bin(decoded[7]) + + # Recover from address using signature + v = deserialize_int(decoded[9]) + r = decoded[10] + s = decoded[11] + + from_address = recover_address_eip1559(decoded, v, r, s, chain_id) + + new( + raw_bytes: raw_bytes, + tx_hash: tx_hash, + from_address: from_address, + to_address: to_address, + nonce: nonce, + gas_limit: gas_limit, + value: value, + data: data, + tx_type: 0x02, + max_fee_per_gas: max_fee, + max_priority_fee_per_gas: max_priority_fee, + gas_price: nil + ) + end + + def self.parse_eip2930_transaction(raw_bytes, tx_hash) + # Skip type byte and decode RLP + rlp_data = raw_bytes.to_bin[1..-1] + decoded = Eth::Rlp.decode(rlp_data) + + # EIP-2930 format: + # [chain_id, nonce, gas_price, gas_limit, to, value, data, access_list, v, r, s] + + chain_id = deserialize_int(decoded[0]) + nonce = deserialize_int(decoded[1]) + gas_price = deserialize_int(decoded[2]) + gas_limit = deserialize_int(decoded[3]) + to_address = decoded[4].empty? ? nil : Address20.from_bin(decoded[4]) + value = deserialize_int(decoded[5]) + data = ByteString.from_bin(decoded[6]) + + # Recover from address using signature + v = deserialize_int(decoded[8]) + r = decoded[9] + s = decoded[10] + + from_address = recover_address_eip2930(decoded, v, r, s, chain_id) + + new( + raw_bytes: raw_bytes, + tx_hash: tx_hash, + from_address: from_address, + to_address: to_address, + nonce: nonce, + gas_limit: gas_limit, + value: value, + data: data, + tx_type: 0x01, + max_fee_per_gas: nil, + max_priority_fee_per_gas: nil, + gas_price: gas_price + ) + end + + def self.parse_legacy_transaction(raw_bytes, tx_hash) + # Legacy transaction - decode RLP directly + decoded = Eth::Rlp.decode(raw_bytes.to_bin) + + # Legacy format: + # [nonce, gas_price, gas_limit, to, value, data, v, r, s] + + nonce = deserialize_int(decoded[0]) + gas_price = deserialize_int(decoded[1]) + gas_limit = deserialize_int(decoded[2]) + to_address = decoded[3].empty? ? nil : Address20.from_bin(decoded[3]) + value = deserialize_int(decoded[4]) + data = ByteString.from_bin(decoded[5]) + + # Recover from address using signature + v = deserialize_int(decoded[6]) + r = decoded[7] + s = decoded[8] + + from_address = recover_address_legacy(decoded[0..5], v, r, s) + + new( + raw_bytes: raw_bytes, + tx_hash: tx_hash, + from_address: from_address, + to_address: to_address, + nonce: nonce, + gas_limit: gas_limit, + value: value, + data: data, + tx_type: 0x00, + max_fee_per_gas: nil, + max_priority_fee_per_gas: nil, + gas_price: gas_price + ) + end + + def self.deserialize_int(data) + return 0 if data.empty? + data.unpack1('H*').to_i(16) + end + + def self.recover_address_eip1559(decoded, v, r, s, chain_id) + # Create signing hash for EIP-1559 + # Exclude signature fields (last 3 elements) + tx_data = decoded[0..8] # Everything except v, r, s + + # Prefix with transaction type + encoded = "\x02" + Eth::Rlp.encode(tx_data) + signing_hash = Eth::Util.keccak256(encoded) + + # Recover public key from signature + # For EIP-1559, v should be 0 or 1, but we need to pass the full signature with v encoded + # The eth.rb gem expects r (32 bytes) + s (32 bytes) + v (variable length hex) + v_hex = v.to_s(16).rjust(2, '0') # Ensure at least 2 hex chars + signature_hex = r.unpack1('H*') + s.unpack1('H*') + v_hex + + public_key = Eth::Signature.recover(signing_hash, signature_hex, chain_id) + # public_key_to_address expects a hex string, not a Secp256k1::PublicKey object + public_key_hex = public_key.is_a?(String) ? public_key : public_key.uncompressed.unpack1('H*') + address = Eth::Util.public_key_to_address(public_key_hex) + # Handle both string and Eth::Address object returns + address_hex = address.is_a?(String) ? address : address.to_s + Address20.from_hex(address_hex) + rescue Secp256k1::DeserializationError => e + raise DecodeError, "Failed to recover EIP-1559 address: #{e.message}" + end + + def self.recover_address_eip2930(decoded, v, r, s, chain_id) + # Create signing hash for EIP-2930 + # Exclude signature fields (last 3 elements) + tx_data = decoded[0..7] # Everything except v, r, s + + # Prefix with transaction type + encoded = "\x01" + Eth::Rlp.encode(tx_data) + signing_hash = Eth::Util.keccak256(encoded) + + # Recover public key from signature + # For EIP-1559, v should be 0 or 1, but we need to pass the full signature with v encoded + # The eth.rb gem expects r (32 bytes) + s (32 bytes) + v (variable length hex) + v_hex = v.to_s(16).rjust(2, '0') # Ensure at least 2 hex chars + signature_hex = r.unpack1('H*') + s.unpack1('H*') + v_hex + + public_key = Eth::Signature.recover(signing_hash, signature_hex, chain_id) + # public_key_to_address expects a hex string, not a Secp256k1::PublicKey object + public_key_hex = public_key.is_a?(String) ? public_key : public_key.uncompressed.unpack1('H*') + address = Eth::Util.public_key_to_address(public_key_hex) + # Handle both string and Eth::Address object returns + address_hex = address.is_a?(String) ? address : address.to_s + Address20.from_hex(address_hex) + end + + def self.recover_address_legacy(tx_data, v, r, s) + # For EIP-155 (v >= 35), reconstruct signing data with chain_id + # For pre-EIP-155 (v = 27/28), use data as-is + if v >= 35 + # Extract chain_id from v + chain_id = (v - 35) / 2 + # Append chain_id, r=empty, s=empty for EIP-155 signing + signing_data = tx_data + [chain_id, "", ""] + else + signing_data = tx_data + end + + # Create signing hash + encoded = Eth::Rlp.encode(signing_data) + signing_hash = Eth::Util.keccak256(encoded) + + # Extract recovery_id from v + recovery_id = if v >= 35 + (v - 35) % 2 + else + v - 27 + end + + # Recover public key from signature + # The eth.rb gem expects r (32 bytes) + s (32 bytes) + v (variable length hex) + # For legacy, pass v as-is + v_hex = v.to_s(16).rjust(2, '0') # Ensure at least 2 hex chars + signature_hex = r.unpack1('H*') + s.unpack1('H*') + v_hex + + # Extract chain_id for legacy transactions if v >= 35 + # For pre-EIP-155, don't pass chain_id (let it use default) + if v >= 35 + tx_chain_id = (v - 35) / 2 + public_key = Eth::Signature.recover(signing_hash, signature_hex, tx_chain_id) + else + # Pre-EIP-155: recover without specifying chain_id + public_key = Eth::Signature.recover(signing_hash, signature_hex) + end + # public_key_to_address expects a hex string, not a Secp256k1::PublicKey object + public_key_hex = public_key.is_a?(String) ? public_key : public_key.uncompressed.unpack1('H*') + address = Eth::Util.public_key_to_address(public_key_hex) + # Handle both string and Eth::Address object returns + address_hex = address.is_a?(String) ? address : address.to_s + Address20.from_hex(address_hex) + end +end diff --git a/app/services/batch_signature_verifier.rb b/app/services/batch_signature_verifier.rb new file mode 100644 index 00000000..a1ddc834 --- /dev/null +++ b/app/services/batch_signature_verifier.rb @@ -0,0 +1,78 @@ +# Signature verification for Facet batches +class BatchSignatureVerifier + include SysConfig + + attr_reader :chain_id + + def initialize(chain_id: ChainIdManager.current_l2_chain_id) + @chain_id = chain_id + end + + # Verify signature for new wire format + # signed_data: [CHAIN_ID:8][VERSION:1][ROLE:1][RLP_TX_LIST] + # signature: 65-byte secp256k1 signature + def verify_wire_format(signed_data, signature) + return nil unless signature + + sig_bytes = signature.is_a?(ByteString) ? signature.to_bin : signature + return nil unless sig_bytes.length == 65 + + # Hash the signed data + message_hash = Eth::Util.keccak256(signed_data) + + # Recover signer from signature + recover_signer(message_hash, sig_bytes) + rescue StandardError => e + if signature_error?(e) + Rails.logger.debug "Signature verification failed: #{e.message}" + nil + else + raise + end + end + + private + + def recover_signer(message_hash, sig_bytes) + # Extract r, s, v from signature + r = sig_bytes[0, 32] + s = sig_bytes[32, 32] + raw_v = sig_bytes[64].ord + + # Normalise recovery id so both {0,1} and {27,28} inputs are accepted + v_normalised = raw_v + v_normalised -= 27 if v_normalised >= 27 + + unless [0, 1].include?(v_normalised) + error_class = defined?(Eth::Signature::SignatureError) ? Eth::Signature::SignatureError : StandardError + raise error_class, "Invalid recovery id #{raw_v}" + end + + v = v_normalised + 27 + + # Create signature for recovery + # The eth.rb gem expects r (32 bytes) + s (32 bytes) + v (variable length hex) + v_hex = v.to_s(16).rjust(2, '0') # Ensure at least 2 hex chars + signature_hex = r.unpack1('H*') + s.unpack1('H*') + v_hex + + # Recover public key and derive address + public_key = Eth::Signature.recover(message_hash, signature_hex) + # public_key_to_address expects a hex string + public_key_hex = public_key.is_a?(String) ? public_key : public_key.uncompressed.unpack1('H*') + address = Eth::Util.public_key_to_address(public_key_hex) + # Handle both string and Eth::Address object returns + address = address.is_a?(String) ? address : address.to_s + + Address20.from_hex(address) + end + + def signature_error?(error) + return true if defined?(Eth::Signature::SignatureError) && error.is_a?(Eth::Signature::SignatureError) + + if defined?(Secp256k1) && Secp256k1.const_defined?(:Error) + return true if error.is_a?(Secp256k1.const_get(:Error)) + end + + false + end +end diff --git a/app/services/blob_provider.rb b/app/services/blob_provider.rb new file mode 100644 index 00000000..b1187c70 --- /dev/null +++ b/app/services/blob_provider.rb @@ -0,0 +1,106 @@ +# Fetches blob data from Ethereum beacon nodes +class BlobProvider + attr_reader :beacon_client, :ethereum_client + + def initialize(beacon_client: nil, ethereum_client: nil) + @beacon_client = beacon_client || EthereumBeaconNodeClient.l1 + @ethereum_client = ethereum_client || EthRpcClient.l1 + + # Validate we have beacon node configured + if ENV['ETHEREUM_BEACON_NODE_API_BASE_URL'].blank? + raise "ETHEREUM_BEACON_NODE_API_BASE_URL must be set for blob support" + end + end + + # List all blob carriers in a block + # Returns array of hashes with tx_hash, tx_index, and versioned_hashes + def list_carriers(block_number, block_data: nil) + # Use provided block data or fetch if not provided + block = block_data || ethereum_client.get_block(block_number, true) + return [] unless block && block['transactions'] + + carriers = [] + block['transactions'].each do |tx| + # Blob versioned hashes are in the transaction itself (type 3 transactions) + next unless tx['blobVersionedHashes'] && !tx['blobVersionedHashes'].empty? + + carriers << { + tx_hash: tx['hash'], + tx_index: tx['transactionIndex'].to_i(16), + versioned_hashes: tx['blobVersionedHashes'] + } + end + + carriers + end + + # Fetch blob data by versioned hash + # Returns ByteString or nil if not found + def get_blob(versioned_hash, block_number:, block_data: nil) + # Fetch raw blob from beacon node + raw_blob = fetch_blob_from_beacon(versioned_hash, block_number: block_number, block_data: block_data) + return nil unless raw_blob + + # Decode from EIP-4844 blob format to get actual data + decoded_data = BlobUtils.from_blobs(blobs: [raw_blob.to_hex]) + + # Return as ByteString + ByteString.from_hex(decoded_data) + end + + private + + def fetch_blob_from_beacon(versioned_hash, block_number:, block_data: nil) + # We must have a block number for deterministic blob fetching + raise ArgumentError, "block_number is required for blob fetching" unless block_number + + # Use provided block data or fetch if not provided + block = block_data || ethereum_client.get_block(block_number, false) + + # Get blob sidecars for this block's slot + begin + sidecars = beacon_client.get_blob_sidecars_for_execution_block(block) + Rails.logger.debug "Block #{block_number}: Found #{sidecars&.size || 0} sidecars" + return nil unless sidecars && !sidecars.empty? + + # Find the sidecar with matching versioned hash + # Sidecars don't have versioned_hash field - must compute from KZG commitment + sidecar = sidecars.find do |s| + kzg = s['kzg_commitment'] || s['kzgCommitment'] + kzg && compute_versioned_hash(kzg) == versioned_hash + end + + if sidecar + # Extract the blob data + blob_data = sidecar['blob'] + + # Most beacon nodes return 0x-hex, but support base64 fallback + if blob_data.start_with?('0x') + # Already hex, return as ByteString + return ByteString.from_hex(blob_data) + else + # Assume base64 + blob_bytes = Base64.decode64(blob_data) + return ByteString.from_bin(blob_bytes) + end + end + end + + Rails.logger.warn "Blob not found for versioned hash #{versioned_hash}" + nil + end + + def compute_versioned_hash(kzg_commitment) + # EIP-4844 versioned hash: 0x01 || sha256(commitment)[1:] + # Drop first byte of SHA256, prepend 0x01 + commitment_bytes = if kzg_commitment.start_with?('0x') + [kzg_commitment[2..-1]].pack('H*') + else + Base64.decode64(kzg_commitment) + end + + hash = Digest::SHA256.digest(commitment_bytes) + # Drop first byte, take remaining 31 bytes + "0x01" + hash[1..31].unpack1('H*') + end +end \ No newline at end of file diff --git a/app/services/eth_block_importer.rb b/app/services/eth_block_importer.rb index 57b2403d..e21477d2 100644 --- a/app/services/eth_block_importer.rb +++ b/app/services/eth_block_importer.rb @@ -1,5 +1,3 @@ -require 'l1_rpc_prefetcher' - class EthBlockImporter include SysConfig include Memery @@ -144,6 +142,7 @@ def set_eth_block_starting_points def import_blocks_until_done MemeryExtensions.clear_all_caches! + ImportProfiler.reset if ImportProfiler.enabled? # Initialize stats tracking stats_start_time = Time.current @@ -202,7 +201,6 @@ def import_blocks_until_done end end - def fetch_block_from_cache(block_number) block_number = [block_number, 0].max @@ -268,25 +266,26 @@ def current_facet_finalized_block end def import_single_block(block_number) + ImportProfiler.start('import_single_block') start = Time.current - # Fetch block data from prefetcher - response = prefetcher.fetch(block_number) - - # Handle cancellation, fetch failure, or block not ready - if response.nil? - raise BlockNotReadyToImportError.new("Block #{block_number} fetch was cancelled or failed") - end - - if response[:error] == :not_ready - raise BlockNotReadyToImportError.new("Block #{block_number} not yet available on L1") + begin + ImportProfiler.start('prefetcher_fetch') + response = prefetcher.fetch(block_number) + rescue L1RpcPrefetcher::BlockFetchError => e + raise BlockNotReadyToImportError.new(e.message) + ensure + ImportProfiler.stop('prefetcher_fetch') end - + + # Extract data from prefetcher response eth_block = response[:eth_block] facet_block = response[:facet_block] facet_txs = response[:facet_txs] - facet_txs.each { |tx| tx.facet_block = facet_block } + facet_txs.each do |facet_tx| + facet_tx.facet_block = facet_block + end # Check for reorg by validating parent hash parent_eth_block = eth_block_cache[block_number - 1] @@ -296,10 +295,12 @@ def import_single_block(block_number) end # Import the L2 block(s) + ImportProfiler.start('propose_facet_block') imported_facet_blocks = propose_facet_block( facet_block: facet_block, facet_txs: facet_txs ) + ImportProfiler.stop('propose_facet_block') logger.debug "Block #{block_number}: Found #{facet_txs.length} facet txs, created #{imported_facet_blocks.length} L2 blocks" @@ -310,6 +311,7 @@ def import_single_block(block_number) eth_block_cache[eth_block.number] = eth_block prune_caches + ImportProfiler.stop('import_single_block') [imported_facet_blocks, [eth_block]] end @@ -363,6 +365,9 @@ def geth_driver def shutdown @prefetcher&.shutdown + rescue => e + logger.error "Error shutting down EthBlockImporter: #{e.message}" + nil end def report_import_stats(blocks_imported_count:, stats_start_time:, stats_start_block:, @@ -409,5 +414,11 @@ def report_import_stats(blocks_imported_count:, stats_start_time:, stats_start_b # Output single message logger.info stats_message + + # Output profiler report if enabled + if ImportProfiler.enabled? && blocks_imported_count % 100 == 0 + ImportProfiler.report + ImportProfiler.reset + end end end diff --git a/app/services/ethereum_beacon_node_client.rb b/app/services/ethereum_beacon_node_client.rb new file mode 100644 index 00000000..5e1c8ed4 --- /dev/null +++ b/app/services/ethereum_beacon_node_client.rb @@ -0,0 +1,98 @@ +class EthereumBeaconNodeClient + include Memery + include RpcErrors + + attr_accessor :base_url + + def initialize(base_url = ENV['ETHEREUM_BEACON_NODE_API_BASE_URL']) + self.base_url = base_url&.chomp('/') + end + + def self.l1 + @_l1_client ||= new(ENV.fetch('ETHEREUM_BEACON_NODE_API_BASE_URL')) + end + + def get_blob_sidecars(block_id) + query_api("eth/v1/beacon/blob_sidecars/#{block_id}") + end + + def get_block(block_id) + query_api("eth/v2/beacon/blocks/#{block_id}") + end + + def get_genesis + query_api("eth/v1/beacon/genesis") + end + memoize :get_genesis + + def get_spec + query_api("eth/v1/config/spec") + end + memoize :get_spec + + # Returns seconds per slot, falling back to 12 if unavailable. + def seconds_per_slot + spec = get_spec + val = spec['SECONDS_PER_SLOT'] || spec['seconds_per_slot'] + (val || 12).to_i + end + + # Compute the beacon slot corresponding to an execution block timestamp + # using: slot = (timestamp - genesis_time) / seconds_per_slot + def slot_for_execution_timestamp(timestamp) + ts = timestamp.to_i + genesis_time = get_genesis.fetch('genesis_time').to_i + ((ts - genesis_time) / seconds_per_slot).to_i + end + + # Convenience: fetch blob sidecars for the beacon slot corresponding to the + # given execution block timestamp (in seconds). + def get_blob_sidecars_for_execution_timestamp(timestamp) + slot = slot_for_execution_timestamp(timestamp) + get_blob_sidecars(slot) + end + + # Convenience: fetch blob sidecars for a given execution block object (as + # returned by JSON-RPC `eth_getBlockByNumber`), using its timestamp. + # Accepts either a raw block result Hash or a wrapper { 'result' => { ... } }. + def get_blob_sidecars_for_execution_block(execution_block) + result = execution_block.is_a?(Hash) && execution_block['result'].is_a?(Hash) ? execution_block['result'] : execution_block + ts_hex_or_int = result.fetch('timestamp') + ts = ts_hex_or_int.is_a?(String) ? ts_hex_or_int.to_i(16) : ts_hex_or_int.to_i + get_blob_sidecars_for_execution_timestamp(ts) + end + + private + + def query_api(endpoint) + # Parse API key from URL if it's embedded in the path (e.g., https://beacon.com/api-key/eth/v1/...) + url = [base_url, endpoint].join('/') + + Retriable.retriable( + tries: 7, + base_interval: 1, + max_interval: 32, + multiplier: 2, + rand_factor: 0.4, + on: [Net::ReadTimeout, Net::OpenTimeout, RpcErrors::HttpError, RpcErrors::ApiError], + on_retry: ->(exception, try, elapsed_time, next_interval) { + Rails.logger.info "Retrying beacon API #{endpoint} (attempt #{try}, next delay: #{next_interval.round(2)}s) - #{exception.message}" + } + ) do + response = HTTParty.get(url) + + unless response.success? + raise RpcErrors::HttpError.new(response.code, response.message) + end + + parsed = response.parsed_response + + # Check for API-level errors in the response + if parsed.is_a?(Hash) && parsed['error'] + raise RpcErrors::ApiError, "API error: #{parsed['error']['message'] || parsed['error']}" + end + + parsed + end + end +end \ No newline at end of file diff --git a/app/services/facet_batch_collector.rb b/app/services/facet_batch_collector.rb new file mode 100644 index 00000000..6c2ac7d3 --- /dev/null +++ b/app/services/facet_batch_collector.rb @@ -0,0 +1,257 @@ +# Collects Facet transactions from all sources (calldata, events, blobs) +# Implements Step 1 of the derivation rules +class FacetBatchCollector + attr_reader :eth_block, :receipts, :blob_provider, :parser, :logger + + CollectorResult = Struct.new(:single_txs, :batches, :stats, keyword_init: true) + + def initialize(eth_block:, receipts:, blob_provider: nil, logger: Rails.logger) + @eth_block = eth_block + @receipts = receipts + @blob_provider = blob_provider || BlobProvider.new + @parser = FacetBatchParser.new(logger: logger) + @logger = logger + end + + # Collect all Facet transactions from the L1 block + # Returns CollectorResult with single_txs and batches arrays + def call + return empty_result unless SysConfig.facet_batch_v2_enabled? + + logger.debug "FacetBatchCollector: Processing block with #{eth_block['transactions'].length} transactions" + + stats = { + single_txs_calldata: 0, + single_txs_events: 0, + batches_calldata: 0, + batches_blobs: 0, + deduped_batches: 0, + missing_blobs: 0 + } + + single_txs = [] + all_batches = [] + + # Index receipts by tx hash for quick lookup + receipt_map = receipts.index_by { |r| r['transactionHash'] } + + # Process each transaction in the block + eth_block['transactions'].each_with_index do |tx, tx_index| + receipt = receipt_map[tx['hash']] + next unless receipt && receipt['status'].to_i(16) == 1 # Skip failed txs + + # Collect V1 single transactions + single_tx = collect_v1_single(tx, receipt, tx_index) + if single_tx + single_txs << single_tx + stats[:single_txs_calldata] += 1 if single_tx[:source] == 'calldata' + stats[:single_txs_events] += 1 if single_tx[:source] == 'events' + end + + # Collect batches from calldata + calldata_batches = collect_batches_from_calldata(tx, tx_index) + if calldata_batches.any? + logger.debug "Found #{calldata_batches.length} batches in tx #{tx['hash']}" + end + all_batches.concat(calldata_batches) + stats[:batches_calldata] += calldata_batches.length + + # Events don't support batches in V2 - only single transactions + end + + # Collect batches from blobs + blob_batches, missing = collect_batches_from_blobs + all_batches.concat(blob_batches) + stats[:batches_blobs] += blob_batches.length + stats[:missing_blobs] += missing + + # Deduplicate batches by content hash + unique_batches = deduplicate_batches(all_batches) + stats[:deduped_batches] = all_batches.length - unique_batches.length + + # Count total Facet transactions + total_txs = single_txs.length + unique_batches.each do |batch| + total_txs += batch.transactions.length + end + stats[:total_transactions] = total_txs + + log_stats(stats) if stats.values.any?(&:positive?) + + CollectorResult.new( + single_txs: single_txs, + batches: unique_batches, + stats: stats + ) + end + + private + + def empty_result + CollectorResult.new(single_txs: [], batches: [], stats: {}) + end + + # Collect V1 single transaction format + def collect_v1_single(tx, receipt, tx_index) + # Check for calldata submission to inbox + if tx['to'] && tx['to'].downcase == EthTransaction::FACET_INBOX_ADDRESS.to_hex.downcase + input = ByteString.from_hex(tx['input']) + + # Skip if contains batch magic (this is a batch, not a single) + return nil if input.to_bin.include?(FacetBatchConstants::MAGIC_PREFIX.to_bin) + + return { + source: 'calldata', + l1_tx_index: tx_index, + tx_hash: tx['hash'], + from_address: tx['from'], # Include L1 sender for mint attribution + payload: input, + events: [] + } + end + + # Check for event-based submission (only first valid event per V1 protocol) + receipt['logs'].each do |log| + next if log['removed'] + next unless log['topics'].length == 1 + next unless log['topics'][0] == EthTransaction::FacetLogInboxEventSig.to_hex + + data = ByteString.from_hex(log['data']) + + # Skip if starts with batch magic + next if data.to_bin.start_with?(FacetBatchConstants::MAGIC_PREFIX.to_bin) + + # V1 protocol: only the FIRST valid event is used + return { + source: 'events', + l1_tx_index: tx_index, + tx_hash: tx['hash'], + payload: nil, # Events don't have a single payload + events: [{ + log_index: log['logIndex'].to_i(16), + address: log['address'], + payload: data + }] + } + end + + nil # No valid V1 transaction found + end + + # Scan calldata for batch magic prefix + def collect_batches_from_calldata(tx, tx_index) + return [] unless tx['input'] && tx['input'].length > 2 + + input = ByteString.from_hex(tx['input']) + source_details = { + tx_hash: tx['hash'], + to: tx['to'] + } + + parser.parse_payload( + input, + tx_index, + FacetBatchConstants::Source::CALLDATA, + source_details + ) + end + + # Collect batches from EIP-4844 blobs + def collect_batches_from_blobs + batches = [] + missing_count = 0 + + # Skip if no blob provider + return [[], 0] unless blob_provider + + # Get list of blob carriers (pass block data to avoid duplicate fetch) + carriers = blob_provider.list_carriers(eth_block['number'].to_i(16), block_data: eth_block) + + carriers.each do |carrier| + carrier[:versioned_hashes].each_with_index do |versioned_hash, blob_index| + # Fetch blob data (returns ByteString by default) + block_number = eth_block['number'].to_i(16) + blob_data = blob_provider.get_blob(versioned_hash, block_number: block_number, block_data: eth_block) + + if blob_data.nil? + logger.warn "Missing blob #{versioned_hash} from tx #{carrier[:tx_hash]}" + missing_count += 1 + next + end + + source_details = { + tx_hash: carrier[:tx_hash], + blob_index: blob_index, + versioned_hash: versioned_hash + } + + batch_list = parser.parse_payload( + blob_data, + carrier[:tx_index], + FacetBatchConstants::Source::BLOB, + source_details + ) + + batches.concat(batch_list) + end + end + + [batches, missing_count] + end + + # Deduplicate batches by content hash, keeping earliest by L1 tx index + def deduplicate_batches(batches) + # Group by content hash + grouped = batches.group_by(&:content_hash) + + # Keep earliest by l1_tx_index for each content hash + grouped.map do |_content_hash, batch_list| + batch_list.min_by(&:l1_tx_index) + end.sort_by(&:l1_tx_index) + end + + def log_stats(stats) + block_num = eth_block['number'].to_i(16) + + # Build a more readable summary + summary_parts = [] + + # Report on L1 transactions + tx_count = eth_block['transactions']&.length || 0 + summary_parts << "#{tx_count} L1 txs" + + # Report on blobs if any + if stats[:batches_blobs] > 0 || stats[:missing_blobs] > 0 + summary_parts << "#{stats[:batches_blobs]} blob batches" + summary_parts << "#{stats[:missing_blobs]} missing blobs" if stats[:missing_blobs] > 0 + end + + # Report on calldata batches + if stats[:batches_calldata] > 0 + summary_parts << "#{stats[:batches_calldata]} calldata batches" + end + + # Report on V1 singles + total_singles = stats[:single_txs_calldata] + stats[:single_txs_events] + if total_singles > 0 + summary_parts << "#{total_singles} V1 singles" + end + + # Report deduplication if any + if stats[:deduped_batches] > 0 + summary_parts << "#{stats[:deduped_batches]} deduped" + end + + # Total Facet transactions found + total_facet_txs = stats[:total_transactions] + if total_facet_txs && total_facet_txs > 0 + summary_parts << "→ #{total_facet_txs} Facet txs" + end + + if summary_parts.any? + logger.info "Block #{block_num}: #{summary_parts.join(', ')}" + else + logger.info "Block #{block_num}: No Facet activity" + end + end +end diff --git a/app/services/facet_batch_parser.rb b/app/services/facet_batch_parser.rb new file mode 100644 index 00000000..e0e6339c --- /dev/null +++ b/app/services/facet_batch_parser.rb @@ -0,0 +1,207 @@ +# Parser for Facet batch format v2 +# Scans payloads for magic prefix, validates, and extracts transactions +class FacetBatchParser + + class ParseError < StandardError; end + class ValidationError < StandardError; end + + attr_reader :chain_id, :logger + + def initialize(chain_id: ChainIdManager.current_l2_chain_id, logger: Rails.logger) + @chain_id = chain_id + @logger = logger + end + + # Parse a payload (calldata, event data, or blob) for batches + # Returns array of ParsedBatch objects + def parse_payload(payload, l1_tx_index, source, source_details = {}) + return [] unless payload + + # logger.debug "FacetBatchParser: Parsing payload of length #{payload.is_a?(ByteString) ? payload.to_bin.length : payload.length}" + + batches = [] + data = payload.is_a?(ByteString) ? payload.to_bin : payload + + # Scan for magic prefix at any offset + offset = 0 + + while (index = data.index(FacetBatchConstants::MAGIC_PREFIX.to_bin, offset)) + logger.debug "FacetBatchParser: Found magic prefix at offset #{index}" + begin + # Need at least full header to proceed + if index + FacetBatchConstants::HEADER_SIZE > data.length + break + end + + # Read and validate chain ID early (before expensive RLP parsing) + chain_id_offset = index + FacetBatchConstants::CHAIN_ID_OFFSET + wire_chain_id = data[chain_id_offset, FacetBatchConstants::CHAIN_ID_SIZE].unpack1('Q>') # uint64 big-endian + + # Skip if wrong chain ID – move past the magic and keep scanning + if wire_chain_id != chain_id + logger.debug "Skipping batch for chain #{wire_chain_id} (expected #{chain_id})" + offset = index + FacetBatchConstants::MAGIC_SIZE + next + end + + batch = parse_batch_at_offset(data, index, l1_tx_index, source, source_details) + batches << batch if batch + + # Enforce max batches per payload + if batches.length >= FacetBatchConstants::MAX_BATCHES_PER_PAYLOAD + logger.warn "Max batches per payload reached (#{FacetBatchConstants::MAX_BATCHES_PER_PAYLOAD})" + break + end + + # Move past this entire batch + # Read length to know how much to skip + role_offset = index + FacetBatchConstants::ROLE_OFFSET + role = data[role_offset, FacetBatchConstants::ROLE_SIZE].unpack1('C') + length_offset = index + FacetBatchConstants::LENGTH_OFFSET + length = data[length_offset, FacetBatchConstants::LENGTH_SIZE].unpack1('N') + total_size = FacetBatchConstants::HEADER_SIZE + length + total_size += FacetBatchConstants::SIGNATURE_SIZE if role == FacetBatchConstants::Role::PRIORITY + offset = index + total_size + rescue ParseError, ValidationError => e + logger.debug "Failed to parse batch at offset #{index}: #{e.message}" + offset = index + FacetBatchConstants::MAGIC_SIZE + end + end + + batches + end + + private + + def parse_batch_at_offset(data, offset, l1_tx_index, source, source_details) + # Read the fixed header fields + # [MAGIC:22][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4] + pos = offset + + # Magic prefix (already validated by caller) + pos += FacetBatchConstants::MAGIC_SIZE + + # Chain ID (uint64 big-endian) + return nil if pos + FacetBatchConstants::CHAIN_ID_SIZE > data.length + wire_chain_id = data[pos, FacetBatchConstants::CHAIN_ID_SIZE].unpack1('Q>') + pos += FacetBatchConstants::CHAIN_ID_SIZE + + # Version (uint8) + return nil if pos + FacetBatchConstants::VERSION_SIZE > data.length + version = data[pos, FacetBatchConstants::VERSION_SIZE].unpack1('C') + pos += FacetBatchConstants::VERSION_SIZE + + # Role (uint8) + return nil if pos + FacetBatchConstants::ROLE_SIZE > data.length + role = data[pos, FacetBatchConstants::ROLE_SIZE].unpack1('C') + pos += FacetBatchConstants::ROLE_SIZE + + # Length (uint32 big-endian) + return nil if pos + FacetBatchConstants::LENGTH_SIZE > data.length + length = data[pos, FacetBatchConstants::LENGTH_SIZE].unpack1('N') + pos += FacetBatchConstants::LENGTH_SIZE + + # Validate header fields + if version != FacetBatchConstants::VERSION + raise ValidationError, "Invalid batch version: #{version} != #{FacetBatchConstants::VERSION}" + end + + if wire_chain_id != chain_id + raise ValidationError, "Invalid chain ID: #{wire_chain_id} != #{chain_id}" + end + + unless [FacetBatchConstants::Role::PERMISSIONLESS, FacetBatchConstants::Role::PRIORITY].include?(role) + raise ValidationError, "Invalid role: #{role}" + end + + if length > FacetBatchConstants::MAX_BATCH_BYTES + raise ParseError, "Batch too large: #{length} > #{FacetBatchConstants::MAX_BATCH_BYTES}" + end + + # Read RLP_TX_LIST + if pos + length > data.length + raise ParseError, "RLP data extends beyond payload: need #{length} bytes, have #{data.length - pos}" + end + rlp_tx_list = data[pos, length] + pos += length + + # Read signature if priority batch + signature = nil + if role == FacetBatchConstants::Role::PRIORITY + if pos + FacetBatchConstants::SIGNATURE_SIZE > data.length + raise ParseError, "Signature extends beyond payload for priority batch" + end + signature = data[pos, FacetBatchConstants::SIGNATURE_SIZE] + end + + # Decode RLP transaction list + transactions = decode_transaction_list(rlp_tx_list) + + # Calculate content hash from CHAIN_ID + VERSION + ROLE + RLP_TX_LIST + SIGNATURE + # Including signature ensures batches with different signatures (e.g., invalid vs valid) don't deduplicate + content_data = [wire_chain_id].pack('Q>') + [version].pack('C') + [role].pack('C') + rlp_tx_list + if signature + content_data += signature + end + content_hash = Hash32.from_bin(Eth::Util.keccak256(content_data)) + + # Verify signature if enabled and priority batch + signer = nil + if role == FacetBatchConstants::Role::PRIORITY + if SysConfig.enable_sig_verify? + # Construct data to sign: [CHAIN_ID:8][VERSION:1][ROLE:1][RLP_TX_LIST] + signed_data = [wire_chain_id].pack('Q>') + [version].pack('C') + [role].pack('C') + rlp_tx_list + signer = verify_signature(signed_data, signature) + raise ValidationError, "Invalid signature for priority batch" unless signer + else + # For testing without signatures + logger.debug "Signature verification disabled for priority batch" + end + end + + # Create ParsedBatch + ParsedBatch.new( + role: role, + signer: signer, + l1_tx_index: l1_tx_index, + source: source, + source_details: source_details, + transactions: transactions, + content_hash: content_hash, + chain_id: wire_chain_id + ) + end + + def decode_transaction_list(rlp_data) + # RLP decode transaction list - expecting an array of raw transaction bytes + decoded = Eth::Rlp.decode(rlp_data) + + unless decoded.is_a?(Array) + raise ParseError, "Invalid transaction list: expected RLP array" + end + + decoded.each_with_index do |tx, index| + unless tx.is_a?(String) + raise ParseError, "Invalid transaction entry at index #{index}: expected byte string" + end + end + + # Validate transaction count + if decoded.length > FacetBatchConstants::MAX_TXS_PER_BATCH + raise ValidationError, "Too many transactions: #{decoded.length} > #{FacetBatchConstants::MAX_TXS_PER_BATCH}" + end + + # Each element should be raw transaction bytes (already EIP-2718 encoded) + decoded.map { |tx| ByteString.from_bin(tx) } + rescue StandardError => e + raise ParseError, "Failed to decode RLP transaction list: #{e.message}" + end + + def verify_signature(signed_data, signature) + return nil unless signature + + # Use BatchSignatureVerifier to verify the signature + verifier = BatchSignatureVerifier.new(chain_id: chain_id) + verifier.verify_wire_format(signed_data, signature) + end +end diff --git a/app/services/facet_block_builder.rb b/app/services/facet_block_builder.rb new file mode 100644 index 00000000..1fc4a5bd --- /dev/null +++ b/app/services/facet_block_builder.rb @@ -0,0 +1,275 @@ +# Builds the final transaction order for an L2 block +# Implements Steps 2-3 of the derivation rules +class FacetBlockBuilder + attr_reader :collected, :l2_block_gas_limit, :get_authorized_signer, :logger + + def initialize(collected:, l2_block_gas_limit:, get_authorized_signer: nil, logger: Rails.logger) + @collected = collected + @l2_block_gas_limit = l2_block_gas_limit + @get_authorized_signer = get_authorized_signer || method(:default_authorized_signer) + @logger = logger + end + + # Build the ordered list of transactions for the L2 block + # Returns array of FacetTransaction objects + def ordered_transactions(l1_block_number) + transactions = [] + + # Step 2: Select priority batch (if any) + priority_batch = select_priority_batch(l1_block_number) + + if priority_batch + logger.info "Selected priority batch from #{priority_batch.source_description} with #{priority_batch.transaction_count} txs" + + # Add all transactions from priority batch first + priority_batch.transactions.each do |tx_bytes| + facet_tx = create_facet_transaction(tx_bytes, priority_batch) + transactions << facet_tx if facet_tx + end + logger.debug "After adding priority batch: #{transactions.length} total transactions" + else + logger.debug "No priority batch selected for block #{l1_block_number}" + end + + # Step 3: Add permissionless transactions + permissionless_sources = collect_permissionless_sources(priority_batch) + + # Sort by L1 transaction index + permissionless_sources.sort_by! { |source| source[:l1_tx_index] } + + # Unwrap transactions from each source + logger.debug "Processing #{permissionless_sources.length} permissionless sources" + permissionless_sources.each do |source| + case source[:type] + when :single + # V1 single transaction + facet_tx = create_v1_transaction(source[:data]) + transactions << facet_tx if facet_tx + when :batch + # Forced batch - unwrap all transactions + logger.debug "Processing forced batch with #{source[:data].transactions.length} transactions" + source[:data].transactions.each do |tx_bytes| + facet_tx = create_facet_transaction(tx_bytes, source[:data]) + if facet_tx + transactions << facet_tx + logger.debug "Added transaction from forced batch, now have #{transactions.length} total" + else + logger.debug "Failed to create transaction from forced batch" + end + end + end + end + + # Build informative summary + if transactions.length > 0 + priority_count = priority_batch ? priority_batch.transaction_count : 0 + forced_count = transactions.length - priority_count + + parts = [] + parts << "#{priority_count} priority" if priority_count > 0 + parts << "#{forced_count} permissionless" if forced_count > 0 + + logger.info "Block #{l1_block_number}: Built with #{transactions.length} txs (#{parts.join(', ')})" + else + logger.debug "Block #{l1_block_number}: No transactions to include" + end + + transactions + end + + private + + def select_priority_batch(l1_block_number) + # Filter for priority batches + priority_batches = collected.batches.select(&:is_priority?) + + return nil if priority_batches.empty? + + # Get authorized signer for this block + authorized_signer = get_authorized_signer.call(l1_block_number) + + # Filter for eligible batches + eligible_batches = if SysConfig.enable_sig_verify? && authorized_signer + priority_batches.select { |b| b.signer == authorized_signer } + else + # For testing without signatures + priority_batches + end + + if eligible_batches.empty? + logger.debug "No eligible priority batches (#{priority_batches.length} priority batches found)" + return nil + end + + # Select batch with lowest L1 transaction index + selected = eligible_batches.min_by(&:l1_tx_index) + + # Gas validation + total_gas = calculate_batch_gas(selected) + priority_limit = (l2_block_gas_limit * SysConfig::PRIORITY_SHARE_BPS) / 10_000 + + if total_gas > priority_limit + logger.warn "Priority batch exceeds gas limit: #{total_gas} > #{priority_limit}, discarding" + return nil + end + + selected + end + + def collect_permissionless_sources(priority_batch) + sources = [] + + logger.debug "Collecting permissionless sources. Total batches: #{collected.batches.length}" + + # Add all forced batches + collected.batches.each do |batch| + logger.debug "Batch role: #{batch.role}, is_priority: #{batch.is_priority?}, tx_count: #{batch.transaction_count}" + if batch.is_priority? + logger.debug "Skipping priority batch with #{batch.transaction_count} txs" + next # Skip priority batches + end + next if priority_batch && batch.content_hash == priority_batch.content_hash # Skip selected priority + + logger.debug "Adding forced batch with #{batch.transaction_count} txs to permissionless sources" + sources << { + type: :batch, + l1_tx_index: batch.l1_tx_index, + data: batch + } + end + + # Add all V1 single transactions + collected.single_txs.each do |single| + sources << { + type: :single, + l1_tx_index: single[:l1_tx_index], + data: single + } + end + + sources + end + + def calculate_batch_gas(batch) + # Calculate total gas for all transactions in batch + # This is simplified - in production would parse each transaction + total_gas = 0 + + batch.transactions.each do |tx_bytes| + # Parse transaction to get gas limit + gas_limit = parse_transaction_gas_limit(tx_bytes) + # Skip transactions with 0 gas (they'll be excluded anyway) + next if gas_limit == 0 + total_gas += gas_limit + end + + total_gas + end + + def parse_transaction_gas_limit(tx_bytes) + # Parse EIP-2718 typed transaction to extract gas limit + tx_type = tx_bytes.to_bin[0].ord + + case tx_type + when 0x02 # EIP-1559 transaction + # Skip type byte and decode RLP + rlp_data = tx_bytes.to_bin[1..-1] + decoded = Eth::Rlp.decode(rlp_data) + + # EIP-1559 format: [chain_id, nonce, max_priority_fee, max_fee, gas_limit, to, value, data, access_list, ...] + # gas_limit is at index 4 + gas_limit = decoded[4].empty? ? 0 : decoded[4].unpack1('H*').to_i(16) + if gas_limit == 0 + logger.warn "Rejecting EIP-1559 transaction with 0 gas limit" + return 0 # Will cause transaction to be excluded + end + gas_limit + + when 0x01 # EIP-2930 transaction (access list) + # Skip type byte and decode RLP + rlp_data = tx_bytes.to_bin[1..-1] + decoded = Eth::Rlp.decode(rlp_data) + + # EIP-2930 format: [chain_id, nonce, gas_price, gas_limit, to, value, data, access_list, ...] + # gas_limit is at index 3 + gas_limit = decoded[3].empty? ? 0 : decoded[3].unpack1('H*').to_i(16) + if gas_limit == 0 + logger.warn "Rejecting EIP-2930 transaction with 0 gas limit" + return 0 # Will cause transaction to be excluded + end + gas_limit + + when 0x00, nil # Legacy transaction (no type byte) + # Legacy transactions don't have a type byte, decode directly + decoded = Eth::Rlp.decode(tx_bytes.to_bin) + + # Legacy format: [nonce, gas_price, gas_limit, to, value, data, v, r, s] + # gas_limit is at index 2 + gas_limit = decoded[2].empty? ? 0 : decoded[2].unpack1('H*').to_i(16) + if gas_limit == 0 + logger.warn "Rejecting legacy transaction with 0 gas limit" + return 0 # Will cause transaction to be excluded + end + gas_limit + + else + # Unknown transaction type, use default + logger.warn "Unknown transaction type: 0x#{tx_type.to_s(16)}" + 21_000 + end + end + + def create_facet_transaction(tx_bytes, batch) + # Create StandardL2Transaction from raw bytes + # These are standard EIP-2718 typed transactions (EIP-1559, EIP-2930, legacy) + StandardL2Transaction.from_raw_bytes(tx_bytes) + rescue StandardL2Transaction::DecodeError => e + batch_hash = batch.respond_to?(:content_hash) ? batch.content_hash.to_hex : 'unknown' + logger.warn "Skipping invalid transaction from batch #{batch_hash}: #{e.message}" + nil + end + + def create_v1_transaction(single_tx_data) + # Create FacetTransaction from V1 single format + + if single_tx_data[:source] == 'calldata' + # Direct calldata submission + # Use L1 sender address for mint attribution + from_address = if single_tx_data[:from_address] + Address20.from_hex(single_tx_data[:from_address]) + else + Address20.from_hex("0x" + "0" * 40) # Fallback to zero if not provided + end + + FacetTransaction.from_payload( + contract_initiated: false, + from_address: from_address, + eth_transaction_input: single_tx_data[:payload], + tx_hash: Hash32.from_hex(single_tx_data[:tx_hash]) + ) + else + # Event-based submission + # Process first event (V1 doesn't support multiple) + event = single_tx_data[:events].first + return nil unless event + + FacetTransaction.from_payload( + contract_initiated: true, + from_address: Address20.from_hex(event[:address]), + eth_transaction_input: event[:payload], + tx_hash: Hash32.from_hex(single_tx_data[:tx_hash]) + ) + end + end + + def default_authorized_signer(block_number) + # Default implementation for testing + # In production, this would query a registry or configuration + + if ENV['PRIORITY_SIGNER_ADDRESS'] + Address20.from_hex(ENV['PRIORITY_SIGNER_ADDRESS']) + else + nil + end + end +end diff --git a/app/services/fct_mint_calculator.rb b/app/services/fct_mint_calculator.rb index 5067f117..819bca68 100644 --- a/app/services/fct_mint_calculator.rb +++ b/app/services/fct_mint_calculator.rb @@ -98,7 +98,7 @@ def calculate_historical_total(block_number) end # --- Core Logic --- - sig { params(facet_txs: T::Array[FacetTransaction], facet_block: FacetBlock).returns(MintPeriod) } + sig { params(facet_txs: T.untyped, facet_block: FacetBlock).returns(MintPeriod) } def assign_mint_amounts(facet_txs, facet_block) # Use legacy mint calculator before the Bluebird fork block if facet_block.number < SysConfig.bluebird_fork_block_number diff --git a/app/services/fct_mint_calculator_albatross.rb b/app/services/fct_mint_calculator_albatross.rb index 86b15c1e..d6ae2967 100644 --- a/app/services/fct_mint_calculator_albatross.rb +++ b/app/services/fct_mint_calculator_albatross.rb @@ -66,10 +66,17 @@ def assign_mint_amounts(facet_txs, facet_block) new_rate = compute_new_rate(facet_block, prev_rate, cumulative_l1_data_gas) facet_txs.each do |tx| - tx.mint = tx.l1_data_gas_used(facet_block.number) * new_rate + # Only FacetTransaction objects have mint field + # StandardL2Transaction objects don't mint + if tx.is_a?(FacetTransaction) + tx.mint = tx.l1_data_gas_used(facet_block.number) * new_rate + end end - batch_l1_data_gas = facet_txs.sum { |tx| tx.l1_data_gas_used(facet_block.number) } + batch_l1_data_gas = facet_txs.sum do |tx| + # Only FacetTransaction objects have l1_data_gas_used + tx.is_a?(FacetTransaction) ? tx.l1_data_gas_used(facet_block.number) : 0 + end if is_first_block_in_period?(facet_block) new_cumulative_l1_data_gas = batch_l1_data_gas diff --git a/app/services/geth_driver.rb b/app/services/geth_driver.rb index af17b103..f2c07337 100644 --- a/app/services/geth_driver.rb +++ b/app/services/geth_driver.rb @@ -22,13 +22,15 @@ def propose_block( finalized_block: ) # Create filler blocks if necessary and update head_block + ImportProfiler.start('create_filler_blocks') filler_blocks = create_filler_blocks( head_block: head_block, new_facet_block: new_facet_block, safe_block: safe_block, finalized_block: finalized_block ) - + ImportProfiler.stop('create_filler_blocks') + head_block = filler_blocks.last || head_block new_facet_block.number = head_block.number + 1 @@ -44,11 +46,13 @@ def propose_block( finalizedBlockHash: finalized_block_hash, } + ImportProfiler.start('assign_mint_amounts') FctMintCalculator.assign_mint_amounts(transactions, new_facet_block) + ImportProfiler.stop('assign_mint_amounts') system_txs = [new_facet_block.attributes_tx] - if new_facet_block.number == 1 + if new_facet_block.number == 1 && !ChainIdManager.on_hoodi? migration_manager_address = "0x22220000000000000000000000000000000000d6" function_selector = ByteString.from_bin(Eth::Util.keccak256('transactionsRequired()').first(4)).to_hex @@ -87,6 +91,13 @@ def propose_block( transactions_with_attributes = system_txs + transactions transaction_payloads = transactions_with_attributes.map(&:to_facet_payload) + + # Log transaction summary + system_count = system_txs.length + user_count = transactions.length + if user_count > 0 || system_count > 1 # 1 is just the attributes tx + Rails.logger.info "Block #{new_facet_block.number}: Proposing #{system_count} system txs, #{user_count} user txs to geth" + end payload_attributes = { timestamp: "0x" + new_facet_block.timestamp.to_s(16), @@ -108,7 +119,9 @@ def propose_block( payload_attributes = ByteString.deep_hexify(payload_attributes) fork_choice_state = ByteString.deep_hexify(fork_choice_state) + ImportProfiler.start('engine_forkchoiceUpdated') fork_choice_response = client.call("engine_forkchoiceUpdatedV#{version}", [fork_choice_state, payload_attributes]) + ImportProfiler.stop('engine_forkchoiceUpdated') if fork_choice_response['error'] raise "Fork choice update failed: #{fork_choice_response['error']}" end @@ -118,7 +131,9 @@ def propose_block( raise "Fork choice update did not return a payload ID" end + ImportProfiler.start('engine_getPayload') get_payload_response = client.call("engine_getPayloadV#{version}", [payload_id]) + ImportProfiler.stop('engine_getPayload') if get_payload_response['error'] raise "Get payload failed: #{get_payload_response['error']}" end @@ -128,6 +143,78 @@ def propose_block( if payload['transactions'].empty? raise "No transactions in returned payload" end + + # Check if geth dropped any transactions we submitted (excluding system txs which can't be dropped) + user_tx_payloads = transactions.map(&:to_facet_payload) + submitted_user_count = user_tx_payloads.size + # Returned count minus system txs (which are always included) + returned_user_count = payload['transactions'].size - system_count + + if submitted_user_count != returned_user_count + dropped_count = submitted_user_count - returned_user_count + Rails.logger.warn("Block #{new_facet_block.number}: Geth rejected #{dropped_count} of #{submitted_user_count} user txs (accepted #{returned_user_count})") + + # Identify which user transactions were dropped by comparing hashes + # Only check user transactions, not system transactions + submitted_user_hashes = user_tx_payloads.map do |tx_payload| + # Convert ByteString to binary string if needed + tx_data = tx_payload.is_a?(ByteString) ? tx_payload.to_bin : tx_payload + ByteString.from_bin(Eth::Util.keccak256(tx_data)).to_hex + end + + # Skip system transactions in returned payload (first system_count txs) + returned_user_payloads = payload['transactions'][system_count..-1] || [] + returned_user_hashes = returned_user_payloads.map do |tx_payload| + # Convert ByteString to binary string if needed + tx_data = tx_payload.is_a?(ByteString) ? tx_payload.to_bin : tx_payload + ByteString.from_bin(Eth::Util.keccak256(tx_data)).to_hex + end + + dropped_hashes = submitted_user_hashes - returned_user_hashes + + if dropped_hashes.any? + Rails.logger.warn("Dropped user transaction hashes: #{dropped_hashes.join(', ')}") + + # Log details about each dropped user transaction for debugging + user_tx_payloads.each_with_index do |tx_payload, index| + # Convert ByteString to binary string if needed + tx_data = tx_payload.is_a?(ByteString) ? tx_payload.to_bin : tx_payload + tx_hash = ByteString.from_bin(Eth::Util.keccak256(tx_data)).to_hex + if dropped_hashes.include?(tx_hash) + # Try to decode the transaction to get more details + begin + decoded_tx = Eth::Tx.decode(tx_data) + + # Handle different transaction types + nonce = if decoded_tx.respond_to?(:nonce) + decoded_tx.nonce + elsif decoded_tx.respond_to?(:signer_nonce) + decoded_tx.signer_nonce + else + "unknown" + end + + from = decoded_tx.respond_to?(:from) ? decoded_tx.from : "unknown" + to = decoded_tx.respond_to?(:destination) ? decoded_tx.destination : + decoded_tx.respond_to?(:to) ? decoded_tx.to : "unknown" + + value = decoded_tx.respond_to?(:value) ? decoded_tx.value : "unknown" + gas_limit = decoded_tx.respond_to?(:gas_limit) ? decoded_tx.gas_limit : "unknown" + gas_price = decoded_tx.respond_to?(:gas_price) ? decoded_tx.gas_price : + decoded_tx.respond_to?(:max_fee_per_gas) ? decoded_tx.max_fee_per_gas : "unknown" + data_size = decoded_tx.respond_to?(:data) ? decoded_tx.data.size : "unknown" + tx_type = decoded_tx.respond_to?(:type) ? decoded_tx.type : "legacy" + + Rails.logger.warn("Dropped tx #{index}: hash=#{tx_hash}, type=#{tx_type}, nonce=#{nonce}, from=#{from}, to=#{to}, value=#{value}, gas_limit=#{gas_limit}, gas_price=#{gas_price}, data_size=#{data_size}") + rescue => e + Rails.logger.warn("Dropped tx #{index}: hash=#{tx_hash} (could not decode: #{e.message})") + end + end + end + end + else + Rails.logger.debug("All #{submitted_user_count} submitted user transactions were included by geth") + end new_payload_request = [payload] @@ -138,7 +225,9 @@ def propose_block( new_payload_request = ByteString.deep_hexify(new_payload_request) + ImportProfiler.start('engine_newPayload') new_payload_response = client.call("engine_newPayloadV#{version}", new_payload_request) + ImportProfiler.stop('engine_newPayload') status = new_payload_response['status'] unless status == 'VALID' @@ -160,7 +249,9 @@ def propose_block( fork_choice_state = ByteString.deep_hexify(fork_choice_state) + ImportProfiler.start('engine_forkchoiceUpdated_finalize') fork_choice_response = client.call("engine_forkchoiceUpdatedV#{version}", [fork_choice_state, nil]) + ImportProfiler.stop('engine_forkchoiceUpdated_finalize') status = fork_choice_response['payloadStatus']['status'] unless status == 'VALID' @@ -221,7 +312,9 @@ def init_command authrpc_port = ENV.fetch('GETH_RPC_URL').split(':').last discovery_port = ENV.fetch('GETH_DISCOVERY_PORT') - genesis_filename = ChainIdManager.on_mainnet? ? "facet-mainnet.json" : "facet-sepolia.json" + network = ChainIdManager.current_l1_network + + genesis_filename = "facet-#{network}.json" command = [ "./facet-chain/unzip_genesis.sh &&", diff --git a/app/services/mint_period.rb b/app/services/mint_period.rb index ba26620f..f9ed6815 100644 --- a/app/services/mint_period.rb +++ b/app/services/mint_period.rb @@ -60,15 +60,19 @@ def remaining_supply [max_supply - total_minted, 0].max.floor.to_r end - sig { params(facet_txs: T::Array[FacetTransaction], current_l1_base_fee: Integer).void } + sig { params(facet_txs: T.untyped, current_l1_base_fee: Integer).void } def assign_mint_amounts(facet_txs, current_l1_base_fee) if blocks_elapsed_in_period >= FctMintCalculator::ADJUSTMENT_PERIOD_TARGET_LENGTH start_new_period(:adjust_up) end facet_txs.each do |tx| - burn = tx.l1_data_gas_used(block_num) * current_l1_base_fee - tx.mint = consume_eth(burn).to_i + # Only FacetTransaction objects have mint field and l1_data_gas_used + # StandardL2Transaction objects don't mint + if tx.is_a?(FacetTransaction) + burn = tx.l1_data_gas_used(block_num) * current_l1_base_fee + tx.mint = consume_eth(burn).to_i + end end end diff --git a/app/services/priority_registry.rb b/app/services/priority_registry.rb new file mode 100644 index 00000000..5f2676b2 --- /dev/null +++ b/app/services/priority_registry.rb @@ -0,0 +1,87 @@ +# Registry for managing priority poster authorization +# Determines who can post priority batches for each L1 block +class PriorityRegistry + include Singleton + + attr_accessor :config + + def initialize + @config = load_config + end + + # Get the authorized signer for a given L1 block number + # Returns Address20 or nil if no signer is authorized + def authorized_signer(l1_block_number) + return nil unless SysConfig.facet_batch_v2_enabled? + + case config[:mode] + when 'static' + # Single static address for all blocks + config[:static_address] ? Address20.from_hex(config[:static_address]) : nil + + when 'rotation' + # Simple rotation between multiple addresses + return nil unless config[:rotation_addresses]&.any? + + addresses = config[:rotation_addresses] + period = config[:rotation_period] || 100 # blocks per rotation + + index = (l1_block_number / period) % addresses.length + Address20.from_hex(addresses[index]) + + when 'mapping' + # Explicit block number to address mapping + return nil unless config[:block_mapping] + + # Find the entry with highest block number <= l1_block_number + applicable = config[:block_mapping] + .select { |entry| entry[:from_block] <= l1_block_number } + .max_by { |entry| entry[:from_block] } + + applicable ? Address20.from_hex(applicable[:address]) : nil + + when 'disabled' + # No priority poster (all batches are forced) + nil + + else + # Default to ENV variable for PoC + ENV['PRIORITY_SIGNER_ADDRESS'] ? Address20.from_hex(ENV['PRIORITY_SIGNER_ADDRESS']) : nil + end + rescue StandardError => e + Rails.logger.warn "Failed to get authorized signer for block #{l1_block_number}: #{e.message}" + nil + end + + # Update configuration (for testing/admin purposes) + def update_config(new_config) + @config = new_config + end + + private + + def load_config + # Load from environment or config file + if ENV['PRIORITY_REGISTRY_CONFIG'] + JSON.parse(ENV['PRIORITY_REGISTRY_CONFIG']).with_indifferent_access + elsif File.exist?(config_file_path) + JSON.parse(File.read(config_file_path)).with_indifferent_access + else + default_config + end + rescue JSON::ParserError, Errno::ENOENT, Errno::EACCES => e + Rails.logger.warn "Failed to load priority registry config: #{e.message}" + default_config + end + + def config_file_path + Rails.root.join('config', 'priority_registry.json') + end + + def default_config + { + mode: ENV['PRIORITY_REGISTRY_MODE'] || 'env', + static_address: ENV['PRIORITY_SIGNER_ADDRESS'] + } + end +end diff --git a/config/derive_facet_blocks.rb b/config/derive_facet_blocks.rb index 0efdb3e8..87468479 100644 --- a/config/derive_facet_blocks.rb +++ b/config/derive_facet_blocks.rb @@ -84,18 +84,24 @@ module Clockwork end every(6.seconds, 'import_blocks_until_done') do - importer = EthBlockImporter.new - - loop do - begin - importer.import_blocks_until_done - rescue EthBlockImporter::ReorgDetectedError - Rails.logger.warn 'Reorg detected – reinitialising EthBlockImporter' - importer = EthBlockImporter.new - retry + $current_importer = EthBlockImporter.new + + begin + loop do + begin + $current_importer.import_blocks_until_done + rescue EthBlockImporter::ReorgDetectedError + Rails.logger.warn 'Reorg detected – reinitialising EthBlockImporter' + $current_importer.shutdown + $current_importer = EthBlockImporter.new + retry + end + + sleep 6 end - - sleep 6 + + ensure + $current_importer&.shutdown end end end diff --git a/config/environments/test.rb b/config/environments/test.rb index d4b44534..a3517cce 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -37,7 +37,7 @@ end config.logger = ActiveSupport::Logger.new(STDOUT) - config.log_level = :info + config.log_level = :debug # Active Storage not in use. diff --git a/config/initializers/dotenv.rb b/config/initializers/dotenv.rb index 019029ba..1f0fd333 100644 --- a/config/initializers/dotenv.rb +++ b/config/initializers/dotenv.rb @@ -3,12 +3,16 @@ Dotenv.load - if ENV['L1_NETWORK'] == 'sepolia' + case ENV['L1_NETWORK'] + when 'sepolia' sepolia_env = Rails.root.join('.env.sepolia') Dotenv.load(sepolia_env) if File.exist?(sepolia_env) - elsif ENV['L1_NETWORK'] == 'mainnet' + when 'mainnet' mainnet_env = Rails.root.join('.env.mainnet') Dotenv.load(mainnet_env) if File.exist?(mainnet_env) + when 'hoodi' + hoodi_env = Rails.root.join('.env.hoodi') + Dotenv.load(hoodi_env) if File.exist?(hoodi_env) else raise "Unknown L1_NETWORK: #{ENV['L1_NETWORK']}" end diff --git a/contracts/foundry.toml b/contracts/foundry.toml index 6c881d3a..475edc2a 100644 --- a/contracts/foundry.toml +++ b/contracts/foundry.toml @@ -5,7 +5,7 @@ libs = ["dependencies"] via_ir = true optimizer = true optimizer_runs = 200 -solc_version = "0.8.24" +# solc_version = "0.8.24" ast = true evm_version = "cancun" gas_limit = 9223372036854775807 diff --git a/contracts/script/DABuilderSetup.s.sol b/contracts/script/DABuilderSetup.s.sol new file mode 100644 index 00000000..269526b6 --- /dev/null +++ b/contracts/script/DABuilderSetup.s.sol @@ -0,0 +1,70 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {Script} from "forge-std/Script.sol"; +import {console2 as console} from "forge-std/console2.sol"; +import "../src/DABuilder/TrustlessProposer.sol"; + +interface IGasTank { + function deposit() external payable; + function deposit(address operator) external payable; + function balances(address operator) external view returns (uint256); +} + +contract DABuilderSetup is Script { + // Holesky addresses from DA Builder docs + address constant GAS_TANK = 0x18Fa15ea0A34a7c4BCA01bf7263b2a9Ac0D32e92; + address constant PROPOSER_MULTICALL = 0x5132dCe9aD675b2ac5E37D69D2bC7399764b5469; + + // Deposit amount (0.1 ETH) + uint256 constant DEPOSIT_AMOUNT = 0.1 ether; + + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + + console.log("Deployer address:", deployer); + console.log("Deployer balance:", deployer.balance); + + vm.startBroadcast(deployerPrivateKey); + + // Step 1: Deploy TrustlessProposer + console.log("\n1. Deploying TrustlessProposer..."); + TrustlessProposer proposer = new TrustlessProposer(PROPOSER_MULTICALL); + address proposerAddress = address(proposer); + console.log(" TrustlessProposer deployed at:", proposerAddress); + + // Step 2: Setup EIP-7702 authorization + console.log("\n2. Setting up EIP-7702 authorization..."); + console.log(" Run this command after deployment:"); + console.log(" cast send --private-key $PRIVATE_KEY \\"); + console.log(" --rpc-url $L1_RPC \\"); + console.log(" --auth", proposerAddress, "\\"); + console.log(" ", deployer, "''"); + console.log("\n This sets EIP-7702 delegation from your EOA to the TrustlessProposer"); + + // Step 3: Fund Gas Tank + console.log("\n3. Funding Gas Tank..."); + IGasTank gasTank = IGasTank(GAS_TANK); + gasTank.deposit{value: DEPOSIT_AMOUNT}(); // Deposits for msg.sender (deployer) + + uint256 balance = gasTank.balances(deployer); + console.log(" Deposited:", DEPOSIT_AMOUNT); + console.log(" Gas Tank balance:", balance); + + vm.stopBroadcast(); + + // Output summary + console.log("\n========================================"); + console.log("DA Builder Setup Complete!"); + console.log("========================================"); + console.log("TrustlessProposer:", proposerAddress); + console.log("EOA Address:", deployer); + console.log("Gas Tank Balance:", balance); + console.log("\nNEXT STEPS:"); + console.log("1. Set EIP-7702 authorization (see command above)"); + console.log("2. Verify with: cast code", deployer); + console.log(" Should return: 0xef0100..."); + console.log("3. Update .env with PROPOSER_ADDRESS=", proposerAddress); + } +} \ No newline at end of file diff --git a/contracts/script/SetupEIP7702.s.sol b/contracts/script/SetupEIP7702.s.sol new file mode 100644 index 00000000..45162f67 --- /dev/null +++ b/contracts/script/SetupEIP7702.s.sol @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.24; + +import {Script} from "forge-std/Script.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +contract SetupEIP7702 is Script { + function run() external { + uint256 deployerPrivateKey = vm.envUint("PRIVATE_KEY"); + address deployer = vm.addr(deployerPrivateKey); + address proposerAddress = vm.envAddress("PROPOSER_ADDRESS"); + + console.log("Setting up EIP-7702 for EOA:", deployer); + console.log("Delegating to TrustlessProposer:", proposerAddress); + + // Get current nonce + uint256 currentNonce = vm.getNonce(deployer); + uint256 authNonce = currentNonce + 1; // Critical: auth nonce = tx nonce + 1 + + console.log("Current nonce:", currentNonce); + console.log("Authorization nonce:", authNonce); + + // Note: Foundry doesn't have native EIP-7702 support yet + // This script outputs the command to run with cast + + console.log("\n========================================"); + console.log("Run this command to set EIP-7702:"); + console.log("========================================\n"); + + console.log("# First, create the authorization signature"); + console.log("# The authorization format is: chainId || nonce || address"); + console.log("# For Holesky (chain 17000):"); + console.log(""); + console.log("export PROPOSER_ADDRESS=", proposerAddress); + console.log("export AUTH_NONCE=", authNonce); + console.log(""); + console.log("# This is a placeholder - actual EIP-7702 signing needs special tooling"); + console.log("# Most wallets don't support it yet, may need custom implementation"); + console.log(""); + console.log("# After setting authorization, verify with:"); + console.log("cast code", deployer, "--rpc-url $L1_RPC"); + console.log("# Expected: 0xef0100", proposerAddress, "(without 0x prefix)"); + } +} \ No newline at end of file diff --git a/contracts/src/DABuilder/TrustlessProposer.sol b/contracts/src/DABuilder/TrustlessProposer.sol new file mode 100644 index 00000000..6c15f5ca --- /dev/null +++ b/contracts/src/DABuilder/TrustlessProposer.sol @@ -0,0 +1,130 @@ +// SPDX-License-Identifier: MIT +pragma solidity 0.8.30; + +import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +interface IProposer { + error LowLevelCallFailed(); + error Unauthorized(); + + function onCall(address _target, bytes calldata _data, uint256 _value) external returns (bool); +} + +/// @title TrustlessProposer +/// @notice A secure proposer implementation that requires cryptographic signatures +/// @dev Uses custom storage layout to prevent conflicts with future account code versions +/// @custom:storage-location keccak256(abi.encode(uint256(keccak256("Spire.TrustlessProposer.1.0.0")) - 1)) & ~bytes32(uint256(0xff)) +contract TrustlessProposer is IProposer, EIP712 layout at 25_732_701_950_170_629_563_862_734_149_613_701_595_693_524_766_703_709_478_375_563_609_458_162_252_544 { + error NonceTooLow(); + error DeadlinePassed(); + error SignatureInvalid(); + error GasLimitExceeded(); + + bytes32 public constant CALL_TYPEHASH = + keccak256("Call(uint256 deadline,uint256 nonce,address target,uint256 value,bytes calldata,uint256 gasLimit)"); + + /// @notice The address of the proposer multicall contract + address public immutable PROPOSER_MULTICALL; + + /// @notice A separate nonce for nested calls from external callers + /// + /// @dev Nonce is used as a uint256 instead of a mapping for gas reasons + uint256 public nestedNonce; + + /// @notice Constructor + /// + /// @param _proposerMulticall The address of the proposer multicall contract + constructor(address _proposerMulticall) EIP712("TrustlessProposer", "1") { + PROPOSER_MULTICALL = _proposerMulticall; + } + + function onCall(address _target, bytes calldata _data, uint256 _value) external returns (bool) { + // The estimated gas used is not perfect but provides a meaningful bound to know if we went over the gas limit + uint256 _startGas = gasleft(); + + if (msg.sender != PROPOSER_MULTICALL && address(this) != msg.sender) revert Unauthorized(); + + // Decode the data parameter which contains: (signature, deadline, nonce, actual_calldata, gasLimit) + (bytes memory _sig, uint256 _deadline, uint256 _nonce, bytes memory _calldata, uint256 _gasLimit) = + abi.decode(_data, (bytes, uint256, uint256, bytes, uint256)); + + if (block.timestamp > _deadline) revert DeadlinePassed(); + if (_nonce != nestedNonce) revert NonceTooLow(); + + // Create the EIP-712 message hash + bytes32 _messageHash = _hashTypedCallData(_deadline, _nonce, _target, _value, _calldata, _gasLimit); + + // Recover the signer from the signature + address _signer = _getSignerFromSignature(_messageHash, _sig); + if (_signer != address(this)) revert SignatureInvalid(); + + nestedNonce++; + + // Execute the actual call + (bool _success,) = _target.call{value: _value}(_calldata); + if (!_success) { + revert LowLevelCallFailed(); + } + + // If gas used is greater than gasLimit, revert + if (_startGas - gasleft() > _gasLimit) { + revert GasLimitExceeded(); + } + + return true; + } + + /// @notice Hashes the typed data for the call (EIP-712) + function _hashTypedCallData( + uint256 _deadline, + uint256 _nonce, + address _target, + uint256 _value, + bytes memory _calldata, + uint256 _gasLimit + ) internal view returns (bytes32) { + return _hashTypedDataV4( + keccak256(abi.encode(CALL_TYPEHASH, _deadline, _nonce, _target, _value, keccak256(_calldata), _gasLimit)) + ); + } + + /// @notice Gets the signer from the signature + /// + /// @param _messageHash The message hash to recover the signer from + /// @param _sig The signature to recover the signer from + /// + /// @return The signer address + function _getSignerFromSignature(bytes32 _messageHash, bytes memory _sig) internal pure returns (address) { + (address _signer, ECDSA.RecoverError _error,) = ECDSA.tryRecover(_messageHash, _sig); + if (_error != ECDSA.RecoverError.NoError) revert SignatureInvalid(); + return _signer; + } + + // ERC-721 / ERC-1155 receiver hooks + function onERC721Received(address, address, uint256, bytes calldata) external pure returns (bytes4) { + return this.onERC721Received.selector; + } + + function onERC1155Received(address, address, uint256, uint256, bytes calldata) external pure returns (bytes4) { + return this.onERC1155Received.selector; + } + + function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata) + external + pure + returns (bytes4) + { + return this.onERC1155BatchReceived.selector; + } + + function supportsInterface(bytes4 _interfaceID) external pure returns (bool) { + bool _supported = _interfaceID == 0x01ffc9a7 // ERC-165 + || _interfaceID == 0x150b7a02 // ERC721TokenReceiver + || _interfaceID == 0x4e2312e0; // ERC-1155 Receiver + return _supported; + } + + receive() external payable {} + fallback() external payable {} +} diff --git a/docker-compose/.env.example b/docker-compose/.env.example new file mode 100644 index 00000000..90670074 --- /dev/null +++ b/docker-compose/.env.example @@ -0,0 +1,40 @@ +# Docker Compose project name (shows in Docker Desktop) +COMPOSE_PROJECT_NAME=facet-node + +# JWT secret for Geth/Node communication +JWT_SECRET="0x0101010101010101010101010101010101010101010101010101010101010101" + +# Genesis configuration +GENESIS_FILE=facet-mainnet.json +GENESIS_TIMESTAMP=1704067200 +GENESIS_MIX_HASH=0x0000000000000000000000000000000000000000000000000000000000000000 + +# L1 Configuration +L1_NETWORK=mainnet # or holesky for testnet +L1_RPC_URL="https://eth_rpc_url" # Replace with your L1 RPC endpoint +L1_GENESIS_BLOCK=21373000 +L1_CHAIN_ID=1 # 1 for mainnet, 17000 for holesky + +# L2 Configuration (auto-discovered from Geth) +# L2_CHAIN_ID=0xface7 # Optional - only set if you want to override + +# Sequencer Configuration +SEQUENCER_PRIVATE_KEY=0x0000000000000000000000000000000000000000000000000000000000000001 + +# DA Builder Configuration (optional - set USE_DA_BUILDER=true to enable) +USE_DA_BUILDER=false +DA_BUILDER_URL=https://da-builder.hoodi.spire.dev/ +FALLBACK_TO_DIRECT=true + +# Batch Configuration +BATCH_INTERVAL_MS=1 # Instant batching for testing - use 3000 for production +MAX_BATCH_SIZE=500 +MAX_BLOB_SIZE=131072 +BLOCK_IMPORT_BATCH_SIZE=10 + +# Performance tuning +GETH_CACHE_SIZE=10000 +RPC_GAS_CAP=500000000 +BLUEBIRD_TIMESTAMP=1751844539 +L1_PREFETCH_FORWARD=200 +L1_PREFETCH_THREADS=10 \ No newline at end of file diff --git a/docker-compose/.env.sample b/docker-compose/.env.sample deleted file mode 100644 index 9e10a91e..00000000 --- a/docker-compose/.env.sample +++ /dev/null @@ -1,7 +0,0 @@ -JWT_SECRET="0x0101010101010101010101010101010101010101010101010101010101010101" -GENESIS_FILE=facet-mainnet.json -L1_NETWORK=mainnet -L1_RPC_URL="https://eth_rpc_url" -L1_GENESIS_BLOCK=21373000 -BLOCK_IMPORT_BATCH_SIZE=10 -BLUEBIRD_TIMESTAMP=1751844539 diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 88fb1ede..ca59440f 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -1,6 +1,6 @@ services: geth: - image: ghcr.io/0xfacet/facet-geth:v2.0.1 + image: ghcr.io/0xfacet/facet-geth:batches environment: JWT_SECRET: ${JWT_SECRET} GENESIS_FILE: ${GENESIS_FILE} @@ -11,8 +11,9 @@ services: CACHE_SIZE: ${GETH_CACHE_SIZE:-10000} volumes: - geth-data:/root/ethereum - ports: - - "8545:8545" + expose: + - "8545" + - "8551" healthcheck: test: ["CMD-SHELL", "geth attach --exec 'eth.blockNumber' http://localhost:8545"] interval: 30s @@ -21,7 +22,7 @@ services: start_period: 10s node: - image: ghcr.io/0xfacet/facet-node:v2.0.1 + image: ghcr.io/0xfacet/facet-node:batches environment: JWT_SECRET: ${JWT_SECRET} L1_NETWORK: ${L1_NETWORK} @@ -32,9 +33,57 @@ services: BLUEBIRD_TIMESTAMP: ${BLUEBIRD_TIMESTAMP:-1754484539} L1_PREFETCH_FORWARD: ${L1_PREFETCH_FORWARD:-200} L1_PREFETCH_THREADS: ${L1_PREFETCH_THREADS:-10} + BLUEBIRD_IMMEDIATE_FORK_MAX_SUPPLY_ETHER: ${BLUEBIRD_IMMEDIATE_FORK_MAX_SUPPLY_ETHER:-1_500_000_000} + ETHEREUM_BEACON_NODE_API_BASE_URL: ${ETHEREUM_BEACON_NODE_API_BASE_URL} + FACET_BATCH_V2_ENABLED: ${FACET_BATCH_V2_ENABLED:-true} + PROFILE_IMPORT: ${PROFILE_IMPORT:-false} + depends_on: + geth: + condition: service_healthy + + sequencer: + image: ghcr.io/0xfacet/facet-sequencer:batches + environment: + # L1 Configuration + L1_RPC_URL: ${L1_RPC_URL} + L1_CHAIN_ID: ${L1_CHAIN_ID:-560048} # Hoodi by default + + # L2 Configuration (points to internal Geth) + L2_RPC_URL: http://geth:8545 + # L2_CHAIN_ID is auto-discovered from Geth + + # Sequencer Configuration + PRIVATE_KEY: ${SEQUENCER_PRIVATE_KEY} + DB_PATH: /data/sequencer.db + PORT: 8545 # Main RPC port + METRICS_PORT: 9090 + + # DA Builder Configuration (optional) + USE_DA_BUILDER: ${USE_DA_BUILDER:-false} + DA_BUILDER_URL: ${DA_BUILDER_URL:-} + FALLBACK_TO_DIRECT: ${FALLBACK_TO_DIRECT:-true} + + # Batch Configuration + MAX_TX_PER_BATCH: ${MAX_TX_PER_BATCH:-500} + MAX_BATCH_SIZE: ${MAX_BATCH_SIZE:-131072} + BATCH_INTERVAL_MS: ${BATCH_INTERVAL_MS:-250} + MAX_PER_SENDER: ${MAX_PER_SENDER:-10} + MAX_PENDING_TXS: ${MAX_PENDING_TXS:-10000} + volumes: + - sequencer-data:/data + ports: + - "8545:8545" + # - "9090:9090" # Metrics endpoint + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8545/health"] + interval: 30s + timeout: 3s + retries: 20 + start_period: 10s depends_on: geth: condition: service_healthy volumes: geth-data: + sequencer-data: diff --git a/docs/test-pages/index.html b/docs/test-pages/index.html new file mode 100644 index 00000000..43ef2ea8 --- /dev/null +++ b/docs/test-pages/index.html @@ -0,0 +1,702 @@ + + + + +Sequencer Test (MetaMask) + +

🔧 Sequencer Test via MetaMask

+ + + + + + +
+ + +
+ + + +
+ + + +
+ + + +
+ + + + +
+ + + +
+ + + + +
+ + + + + + + + + + + +

+
+
+
\ No newline at end of file
diff --git a/lib/blob_utils.rb b/lib/blob_utils.rb
new file mode 100644
index 00000000..4fd9643a
--- /dev/null
+++ b/lib/blob_utils.rb
@@ -0,0 +1,117 @@
+# EIP-4844 Blob helpers (parity with viem)
+# - to_blobs: transforms arbitrary data (hex or bytes) into 4844 blobs.
+# - from_blobs: reconstructs original data from blobs (hex in => hex out, bytes in => bytes out).
+
+module BlobUtils
+  # Parameters from the EIP-4844 spec (match viem/src/constants/blob.ts)
+  BLOBS_PER_TRANSACTION = 6
+  BYTES_PER_FIELD_ELEMENT = 32
+  FIELD_ELEMENTS_PER_BLOB = 4096
+  BYTES_PER_BLOB = BYTES_PER_FIELD_ELEMENT * FIELD_ELEMENTS_PER_BLOB
+  MAX_BYTES_PER_TRANSACTION = BYTES_PER_BLOB * BLOBS_PER_TRANSACTION - 1 - (1 * FIELD_ELEMENTS_PER_BLOB * BLOBS_PER_TRANSACTION)
+
+  class BlobSizeTooLargeError < StandardError; end
+  class EmptyBlobError < StandardError; end
+
+  # Transform arbitrary data (hex string starting with 0x or a raw byte String) into blobs.
+  # Returns an array of hex strings (each starting with 0x) representing blobs.
+  def self.to_blobs(data:)
+    # Normalize input to raw bytes
+    bytes =
+      if data.is_a?(String) && data.match?(/\A0x/i)
+        hex = data.sub(/\A0x/i, '')
+        raise EmptyBlobError if hex.empty?
+        [hex].pack('H*')
+      elsif data.is_a?(String)
+        data.b
+      else
+        # Fall back to String conversion
+        data.to_s.b
+      end
+
+    raise EmptyBlobError if bytes.bytesize == 0
+    raise BlobSizeTooLargeError if bytes.bytesize > MAX_BYTES_PER_TRANSACTION
+
+    blobs = []
+    position = 0
+    active = true
+
+    while active
+      blob = []
+      size = 0
+
+      while size < FIELD_ELEMENTS_PER_BLOB
+        segment = bytes.byteslice(position, BYTES_PER_FIELD_ELEMENT - 1) # 31-byte segment
+
+        # Leading zero so field element does not overflow BLS modulus
+        blob << 0x00
+        blob.concat(segment ? segment.bytes : [])
+
+        # If segment is underfilled (<31), append terminator and finish
+        if segment.nil? || segment.bytesize < (BYTES_PER_FIELD_ELEMENT - 1)
+          blob << 0x80
+          active = false
+          break
+        end
+
+        size += 1
+        position += (BYTES_PER_FIELD_ELEMENT - 1)
+      end
+
+      # Right-pad blob with zeros
+      if blob.length < BYTES_PER_BLOB
+        blob.fill(0x00, blob.length...BYTES_PER_BLOB)
+      end
+
+      blobs << ("0x" + blob.pack('C*').unpack1('H*'))
+    end
+
+    blobs
+  end
+
+  # Transform blobs (array of hex strings with 0x prefix or byte Strings) back into original data.
+  # If input blobs are hex strings, returns a hex string (0x...)
+  # If input blobs are bytes, returns a raw byte String.
+  def self.from_blobs(blobs:)
+    return (blobs.first.is_a?(String) ? '0x' : ''.b) if blobs.nil? || blobs.empty?
+
+    # Determine output format based on input type (match viem default behavior)
+    return_hex = blobs.first.is_a?(String)
+
+    active = true
+    out = []
+
+    blobs.each do |blob|
+      bytes = if blob.is_a?(String)
+        [blob.sub(/\A0x/i, '')].pack('H*').bytes
+      else
+        blob.bytes
+      end
+
+      pos = 0
+      while active && pos < bytes.length
+        # Skip leading 0x00 of the field element
+        pos += 1
+
+        consume = [BYTES_PER_FIELD_ELEMENT - 1, bytes.length - pos].min
+        consume.times do
+          byte = bytes[pos]
+          pos += 1
+
+          remaining = bytes[pos..-1] || []
+          # Match viem: terminator if this byte is 0x80 and there is no other 0x80 in the rest of the current blob
+          is_terminator = (byte == 0x80) && !remaining.include?(0x80)
+          if is_terminator
+            active = false
+            break
+          end
+
+          out << byte
+        end
+      end
+    end
+
+    packed = out.pack('C*')
+    return_hex ? ("0x" + packed.unpack1('H*')) : packed
+  end
+end
\ No newline at end of file
diff --git a/lib/chain_id_manager.rb b/lib/chain_id_manager.rb
index 6677d958..9f57e43d 100644
--- a/lib/chain_id_manager.rb
+++ b/lib/chain_id_manager.rb
@@ -1,66 +1,71 @@
 module ChainIdManager
   extend self
   include Memery
-  
+
   MAINNET_CHAIN_ID = 1
   SEPOLIA_CHAIN_ID = 11155111
-  
+  HOODI_CHAIN_ID = 560048
+
   FACET_MAINNET_CHAIN_ID = 0xface7
   FACET_SEPOLIA_CHAIN_ID = 0xface7a
-  
+  FACET_HOODI_CHAIN_ID = 0xface7b
+
   def current_l2_chain_id
     candidate = l2_chain_id_from_l1_network_name(current_l1_network)
-    
     according_to_geth = GethDriver.client.call('eth_chainId').to_i(16)
-    
     unless according_to_geth == candidate
       raise "Invalid L2 chain ID: #{candidate} (according to geth: #{according_to_geth})"
     end
-    
     candidate
   end
   memoize :current_l2_chain_id
-  
+
   def l2_chain_id_from_l1_network_name(l1_network_name)
     case l1_network_name
     when 'mainnet'
       FACET_MAINNET_CHAIN_ID
     when 'sepolia'
       FACET_SEPOLIA_CHAIN_ID
+    when 'hoodi'
+      FACET_HOODI_CHAIN_ID
     else
       raise "Unknown L1 network name: #{l1_network_name}"
     end
   end
-  
+
   def on_sepolia?
     current_l1_network == 'sepolia'
   end
-  
+
+  def on_hoodi?
+    current_l1_network == 'hoodi'
+  end
+
   def current_l1_network
     l1_network = ENV.fetch('L1_NETWORK')
-    
-    unless ['sepolia', 'mainnet'].include?(l1_network)
+    unless ['sepolia', 'mainnet', 'hoodi'].include?(l1_network)
       raise "Invalid L1 network: #{l1_network}"
     end
-    
     l1_network
   end
-  
+
   def current_l1_chain_id
     case current_l1_network
     when 'sepolia'
       SEPOLIA_CHAIN_ID
     when 'mainnet'
       MAINNET_CHAIN_ID
+    when 'hoodi'
+      HOODI_CHAIN_ID
     else
       raise "Unknown L1 network: #{current_l1_network}"
     end
   end
-  
+
   def on_mainnet?
     current_l1_network == 'mainnet'
   end
-  
+
   def on_testnet?
     !on_mainnet?
   end
diff --git a/lib/eth_rpc_client.rb b/lib/eth_rpc_client.rb
index 3f550fd5..30185b46 100644
--- a/lib/eth_rpc_client.rb
+++ b/lib/eth_rpc_client.rb
@@ -1,15 +1,5 @@
 class EthRpcClient
-  class HttpError < StandardError
-    attr_reader :code, :http_message
-    
-    def initialize(code, http_message)
-      @code = code
-      @http_message = http_message
-      super("HTTP error: #{code} #{http_message}")
-    end
-  end
-  class ApiError < StandardError; end
-  class MethodRequiredError < StandardError; end
+  include RpcErrors
   attr_accessor :base_url
 
   def initialize(base_url = ENV['L1_RPC_URL'])
@@ -45,6 +35,13 @@ def get_nonce(address, block_number = "latest")
     ).to_i(16)
   end
   
+  def get_transaction_count(address, block_number = "latest")
+    query_api(
+      method: 'eth_getTransactionCount',
+      params: [address, block_number]
+    ).to_i(16)
+  end
+  
   def get_chain_id
     query_api(method: 'eth_chainId').to_i(16)
   end
@@ -102,7 +99,7 @@ def get_transaction_receipt(transaction_hash)
   def get_block_number
     query_api(method: 'eth_blockNumber').to_i(16)
   end
-
+  
   def query_api(method = nil, params = [], **kwargs)
     if kwargs.present?
       method = kwargs[:method]
@@ -110,7 +107,7 @@ def query_api(method = nil, params = [], **kwargs)
     end
     
     unless method
-      raise MethodRequiredError, "Method is required"
+      raise RpcErrors::MethodRequiredError, "Method is required"
     end
     
     data = {
@@ -128,7 +125,7 @@ def query_api(method = nil, params = [], **kwargs)
       max_interval: 32,
       multiplier: 2,
       rand_factor: 0.4,
-      on: [Net::ReadTimeout, Net::OpenTimeout, HttpError, ApiError],
+      on: [Net::ReadTimeout, Net::OpenTimeout, RpcErrors::HttpError, RpcErrors::ApiError],
       on_retry: ->(exception, try, elapsed_time, next_interval) {
         Rails.logger.info "Retrying #{method} (attempt #{try}, next delay: #{next_interval.round(2)}s) - #{exception.message}"
       }
@@ -136,13 +133,13 @@ def query_api(method = nil, params = [], **kwargs)
       response = HTTParty.post(url, body: data.to_json, headers: headers)
       
       if response.code != 200
-        raise HttpError.new(response.code, response.message)
+        raise RpcErrors::HttpError.new(response.code, response.message)
       end
 
       parsed_response = JSON.parse(response.body, max_nesting: false)
       
       if parsed_response['error']
-        raise ApiError, "API error: #{parsed_response.dig('error', 'message') || 'Unknown API error'}"
+        raise RpcErrors::ApiError, "API error: #{parsed_response.dig('error', 'message') || 'Unknown API error'}"
       end
 
       parsed_response['result']
diff --git a/lib/import_profiler.rb b/lib/import_profiler.rb
new file mode 100644
index 00000000..3152f599
--- /dev/null
+++ b/lib/import_profiler.rb
@@ -0,0 +1,122 @@
+class ImportProfiler
+  include Singleton
+
+  def self.start(label)
+    instance.start(label)
+  end
+
+  def self.stop(label)
+    instance.stop(label)
+  end
+
+
+  def self.report
+    instance.report
+  end
+
+  def self.reset
+    instance.reset
+  end
+
+  def self.enabled?
+    instance.enabled?
+  end
+
+  def initialize
+    @enabled = ENV['PROFILE_IMPORT'] == 'true'
+    reset
+  end
+
+  def enabled?
+    @enabled
+  end
+
+  def start(label)
+    return unless @enabled
+
+    # Support nested timing by using a per-thread stack
+    thread_id = Thread.current.object_id
+    @start_stack[thread_id] ||= Concurrent::Map.new
+    @start_stack[thread_id][label] ||= Concurrent::Array.new
+    @start_stack[thread_id][label].push(Process.clock_gettime(Process::CLOCK_MONOTONIC))
+  end
+
+  def stop(label)
+    return nil unless @enabled
+
+    thread_id = Thread.current.object_id
+    return nil unless @start_stack[thread_id] && @start_stack[thread_id][label] && !@start_stack[thread_id][label].empty?
+
+    start_time = @start_stack[thread_id][label].pop
+    elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
+
+    @timings[label] ||= Concurrent::Array.new
+    @timings[label] << elapsed
+
+    # Clean up empty stacks
+    if @start_stack[thread_id][label].empty?
+      @start_stack[thread_id].delete(label)
+      @start_stack.delete(thread_id) if @start_stack[thread_id].empty?
+    end
+
+    elapsed
+  end
+
+
+  def report
+    return unless @enabled
+    return if @timings.empty?
+
+    Rails.logger.info "=" * 100
+    Rails.logger.info "IMPORT PROFILE REPORT"
+    Rails.logger.info "=" * 100
+
+    # Calculate totals and averages
+    report_data = @timings.each_pair.map do |label, times|
+      {
+        label: label,
+        count: times.size,
+        total: times.sum.round(3),
+        avg: (times.sum / times.size).round(3),
+        min: times.min.round(3),
+        max: times.max.round(3),
+        total_ms: (times.sum * 1000).round(1)
+      }
+    end
+
+    # Sort by total time descending
+    report_data.sort_by! { |d| -d[:total] }
+
+    # Find the grand total
+    grand_total = report_data.sum { |d| d[:total] }
+
+    # Print table header
+    Rails.logger.info sprintf("%-45s %8s %10s %8s %8s %8s %10s %6s",
+                              "Operation", "Count", "Total(ms)", "Avg(ms)", "Min(ms)", "Max(ms)", "Total(s)", "Pct%")
+    Rails.logger.info "-" * 110
+
+    # Print each timing
+    report_data.each do |data|
+      pct = grand_total > 0 ? ((data[:total] / grand_total) * 100).round(1) : 0
+      Rails.logger.info sprintf("%-45s %8d %10.1f %8.1f %8.1f %8.1f %10.3f %6.1f%%",
+                                data[:label],
+                                data[:count],
+                                data[:total_ms],
+                                data[:avg] * 1000,
+                                data[:min] * 1000,
+                                data[:max] * 1000,
+                                data[:total],
+                                pct)
+    end
+
+    Rails.logger.info "-" * 110
+    Rails.logger.info sprintf("%-45s %8s %10.1f %8s %8s %8s %10.3f",
+                              "TOTAL", "", grand_total * 1000, "", "", "", grand_total)
+    Rails.logger.info "=" * 100
+  end
+
+  def reset
+    @timings = Concurrent::Map.new
+    @start_stack = Concurrent::Map.new
+  end
+end
diff --git a/lib/l1_rpc_prefetcher.rb b/lib/l1_rpc_prefetcher.rb
index 004c79d6..a3a4df68 100644
--- a/lib/l1_rpc_prefetcher.rb
+++ b/lib/l1_rpc_prefetcher.rb
@@ -1,7 +1,9 @@
-require 'concurrent'
-require 'retriable'
-
 class L1RpcPrefetcher
+  include Memery
+
+  # Raised when a block fetch cannot complete (timeout, not ready, etc)
+  class BlockFetchError < StandardError; end
+  
   def initialize(ethereum_client:,
                  ahead: ENV.fetch('L1_PREFETCH_FORWARD', Rails.env.test? ? 5 : 20).to_i,
                  threads: ENV.fetch('L1_PREFETCH_THREADS', Rails.env.test? ? 2 : 2).to_i)
@@ -12,24 +14,31 @@ def initialize(ethereum_client:,
     # Thread-safe collections and pool
     @pool = Concurrent::FixedThreadPool.new(threads)
     @promises = Concurrent::Map.new
+    @last_chain_tip = current_l1_block_number
 
     Rails.logger.info "L1RpcPrefetcher initialized with #{threads} threads"
   end
-
+  
   def ensure_prefetched(from_block)
-    to_block = from_block + @ahead
+    distance_from_last_tip = @last_chain_tip - from_block
+    
+    current_tip = if distance_from_last_tip > 10
+      cached_current_l1_block_number
+    else
+      current_l1_block_number
+    end
+    
+    # Don't prefetch beyond chain tip
+    to_block = [from_block + @ahead, current_tip].min
+
     # Only create promises for blocks we don't have yet
     blocks_to_fetch = (from_block..to_block).reject { |n| @promises.key?(n) }
 
     return if blocks_to_fetch.empty?
 
-    # Only enqueue a reasonable number at once to avoid overwhelming the promise system
-    max_to_enqueue = [@threads * 10, 50].min
+    Rails.logger.debug "Enqueueing #{blocks_to_fetch.size} blocks: #{blocks_to_fetch.first}..#{blocks_to_fetch.last}"
 
-    to_enqueue = blocks_to_fetch.first(max_to_enqueue)
-    Rails.logger.debug "Enqueueing #{to_enqueue.size} of #{blocks_to_fetch.size} blocks: #{to_enqueue.first}..#{to_enqueue.last}"
-
-    to_enqueue.each { |block_number| enqueue_single(block_number) }
+    blocks_to_fetch.each { |block_number| enqueue_single(block_number) }
   end
 
   def fetch(block_number)
@@ -43,21 +52,18 @@ def fetch(block_number)
 
     Rails.logger.debug "Fetching block #{block_number}, promise state: #{promise.state}"
 
-    begin
-      result = promise.value!(timeout)
-      Rails.logger.debug "Got result for block #{block_number}"
-
-      # Clean up :not_ready promises so they can be retried
-      if result[:error] == :not_ready
-        @promises.delete(block_number)
-      end
+    result = promise.value!(timeout)
 
-      result
-    rescue Concurrent::TimeoutError => e
-      Rails.logger.error "Timeout fetching block #{block_number} after #{timeout}s"
+    if result.nil? || result == :not_ready_sentinel
       @promises.delete(block_number)
-      raise
+      message = result.nil? ?
+        "Block #{block_number} fetch timed out after #{timeout}s" :
+        "Block #{block_number} not yet available on L1"
+      raise BlockFetchError.new(message)
     end
+
+    Rails.logger.debug "Got result for block #{block_number}"
+    result
   end
 
   def clear_older_than(min_keep)
@@ -96,19 +102,28 @@ def stats
       threads_queued: @pool.queue_length
     }
   end
-
+  
   def shutdown
     @pool.shutdown
-    if @pool.wait_for_termination(30)
-      Rails.logger.info "L1 RPC Prefetcher thread pool shut down successfully"
-    else
-      Rails.logger.warn "L1 RPC Prefetcher shutdown timed out, forcing kill"
-      @pool.kill
-    end
+    terminated = @pool.wait_for_termination(3)
+    @pool.kill unless terminated
+  
+    # Explicitly remove any outstanding promises
+    @promises.each_pair { |_, pr| pr.cancel if pr.pending? rescue nil }
+    @promises.clear
+  
+    Rails.logger.info(
+      terminated ?
+        'L1 RPC Prefetcher thread pool shut down successfully' :
+        "L1 RPC Prefetcher shutdown timed out after 3s, pool killed"
+    )
+  
+    terminated
+  rescue StandardError => e
+    Rails.logger.error("Error during L1RpcPrefetcher shutdown: #{e.message}\n#{e.backtrace.join("\n")}")
+    false
   end
 
-  private
-
   def enqueue_single(block_number)
     @promises.compute_if_absent(block_number) do
       Rails.logger.debug "Creating promise for block #{block_number}"
@@ -118,7 +133,6 @@ def enqueue_single(block_number)
         fetch_job(block_number)
       end.rescue do |e|
         Rails.logger.error "Prefetch failed for block #{block_number}: #{e.message}"
-        # Clean up failed promise so it can be retried
         @promises.delete(block_number)
         raise e
       end
@@ -135,20 +149,66 @@ def fetch_job(block_number)
       # Handle case where block doesn't exist yet (normal when caught up)
       if block.nil?
         Rails.logger.debug "Block #{block_number} not yet available on L1"
-        return { error: :not_ready, block_number: block_number }
+        return :not_ready_sentinel
       end
 
       receipts = client.get_transaction_receipts(block_number)
 
       eth_block = EthBlock.from_rpc_result(block)
       facet_block = FacetBlock.from_eth_block(eth_block)
-      facet_txs = EthTransaction.facet_txs_from_rpc_results(block, receipts)
+
+      # Use batch collection v2 if enabled, otherwise use v1
+      facet_txs = if SysConfig.facet_batch_v2_enabled?
+        collect_facet_transactions_v2(block, receipts)
+      else
+        EthTransaction.facet_txs_from_rpc_results(block, receipts)
+      end
 
       {
         eth_block: eth_block,
         facet_block: facet_block,
-        facet_txs: facet_txs
+        facet_txs: facet_txs,
+        block_result: block,
+        receipt_result: receipts
       }
     end
   end
-end
\ No newline at end of file
+
+  # Collect Facet transactions using the v2 batch-aware system
+  def collect_facet_transactions_v2(block_result, receipt_result)
+    block_number = block_result['number'].to_i(16)
+
+    # Use the batch collector to find all transactions
+    collector = FacetBatchCollector.new(
+      eth_block: block_result,
+      receipts: receipt_result,
+      blob_provider: blob_provider,
+      logger: Rails.logger
+    )
+
+    collected = collector.call
+
+    # Build the final transaction order
+    builder = FacetBlockBuilder.new(
+      collected: collected,
+      l2_block_gas_limit: SysConfig::L2_BLOCK_GAS_LIMIT,
+      get_authorized_signer: ->(block_num) { PriorityRegistry.instance.authorized_signer(block_num) },
+      logger: Rails.logger
+    )
+
+    builder.ordered_transactions(block_number)
+  end
+
+  def blob_provider
+    @blob_provider ||= BlobProvider.new
+  end
+  
+  def current_l1_block_number
+    @last_chain_tip = @eth.get_block_number
+  end
+  
+  def cached_current_l1_block_number
+    current_l1_block_number
+  end
+  memoize :cached_current_l1_block_number, ttl: 12.seconds
+end
diff --git a/lib/rpc_errors.rb b/lib/rpc_errors.rb
new file mode 100644
index 00000000..76f8c5be
--- /dev/null
+++ b/lib/rpc_errors.rb
@@ -0,0 +1,14 @@
+module RpcErrors
+  class HttpError < StandardError
+    attr_reader :code, :http_message
+
+    def initialize(code, http_message)
+      @code = code
+      @http_message = http_message
+      super("HTTP error: #{code} #{http_message}")
+    end
+  end
+
+  class ApiError < StandardError; end
+  class MethodRequiredError < StandardError; end
+end
\ No newline at end of file
diff --git a/lib/sys_config.rb b/lib/sys_config.rb
index 7b8dd8fe..d7bc5b2d 100644
--- a/lib/sys_config.rb
+++ b/lib/sys_config.rb
@@ -4,6 +4,22 @@ module SysConfig
   L2_BLOCK_GAS_LIMIT = Integer(ENV.fetch('L2_BLOCK_GAS_LIMIT', 200_000_000))
   L2_BLOCK_TIME = 12
   
+  # Priority share configuration
+  PRIORITY_SHARE_BPS = Integer(ENV.fetch('PRIORITY_SHARE_BPS', 8000))  # 80% default
+  
+  # Feature flags - module methods
+  def self.facet_batch_v2_enabled?
+    ENV.fetch('FACET_BATCH_V2_ENABLED', 'false').casecmp?('true')
+  end
+  
+  def self.enable_sig_verify?
+    ENV.fetch('ENABLE_SIG_VERIFY', 'false').casecmp?('true')
+  end
+  
+  def self.priority_gas_limit
+    (L2_BLOCK_GAS_LIMIT * PRIORITY_SHARE_BPS) / 10_000
+  end
+  
   def block_gas_limit(block)
     if block.number == 1
       migration_gas + L2_BLOCK_GAS_LIMIT
diff --git a/package.json b/package.json
new file mode 100644
index 00000000..dbd78d3d
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+  "name": "facet-blob-e2e",
+  "version": "1.0.0",
+  "type": "module",
+  "description": "E2E testing for Facet blob transactions",
+  "scripts": {
+    "send-blob": "node scripts/send_facet_blob_tx.mjs"
+  },
+  "dependencies": {
+    "viem": "2.37.5",
+    "c-kzg": "4.1.0",
+    "dotenv": "17.2.2"
+  }
+}
\ No newline at end of file
diff --git a/scripts/send_facet_blob_tx.mjs b/scripts/send_facet_blob_tx.mjs
new file mode 100644
index 00000000..673b057a
--- /dev/null
+++ b/scripts/send_facet_blob_tx.mjs
@@ -0,0 +1,465 @@
+import { 
+  createWalletClient, 
+  http, 
+  createPublicClient,
+  parseGwei, 
+  toBlobs,
+  toHex,
+  keccak256,
+  numberToHex,
+  concatHex,
+  size,
+  encodeFunctionData,
+  parseAbi,
+  encodeAbiParameters,
+  parseAbiParameters
+} from 'viem';
+import { sepolia, hoodi, holesky } from 'viem/chains';
+import { privateKeyToAccount } from 'viem/accounts';
+import { toRlp } from 'viem/utils';
+import cKzg from 'c-kzg';
+import { mainnetTrustedSetupPath } from 'viem/node';
+import dotenv from 'dotenv';
+// no fs/crypto needed
+import { formatGwei } from 'viem';
+
+dotenv.config({ path: '.env.node' });
+
+// Configuration from environment
+const CHAIN = process.env.CHAIN;
+const PRIVATE_KEY = process.env.PRIVATE_KEY;
+const RPC_URL = process.env.RPC_URL;
+const DA_BUILDER_URL = process.env.DA_BUILDER_URL; // optional: submit via DA Builder if set
+const L2_CHAIN_ID = parseInt(process.env.L2_CHAIN_ID, 16); // Default Facet L2 chain ID
+const FACET_MAGIC_PREFIX = process.env.FACET_MAGIC_PREFIX || '0x0000000000012345';
+const PROPOSER_ADDRESS = process.env.PROPOSER_ADDRESS; // Required for DA Builder mode
+
+console.log('Environment loaded:', {
+  CHAIN,
+  RPC_URL,
+  L2_CHAIN_ID: `0x${L2_CHAIN_ID.toString(16)}`,
+  HAS_PRIVATE_KEY: !!PRIVATE_KEY
+});
+
+// Chain selection
+const chain = CHAIN === 'hoodi' ? hoodi : CHAIN === 'holesky' ? holesky : sepolia;
+
+// Helper to encode minimal-length big-endian integers for RLP
+// const toMinimalHex = (n) => {
+//   if (n === 0 || n === 0n) return '0x';
+//   const hex = typeof n === 'bigint' ? toHex(n) : numberToHex(n);
+//   // Remove leading zeros but keep at least one byte
+//   return hex === '0x00' ? '0x' : hex.replace(/^0x0+/, '0x');
+// };
+
+function buildFacetBatchData(version, chainId, role, targetL1Block, transactions, extraData = '0x') {
+  // FacetBatchData = [version, chainId, role, targetL1Block, transactions[], extraData]
+  return [
+    toHex(version),     // uint8 version
+    toHex(chainId),           // uint256 chainId (minimal encoding)
+    toHex(role),        // uint8 role (0=FORCED, 1=PRIORITY) per our usage
+    toHex(targetL1Block),     // uint256 targetL1Block (minimal encoding)
+    transactions,                    // RLP list of byte strings
+    extraData                        // bytes extraData
+  ];
+}
+
+function encodeFacetBatch(batchData, signature = null) {
+  // FacetBatch = [FacetBatchData, signature?]
+  const outer = signature ? [batchData, signature] : [batchData];
+  return toRlp(outer);
+}
+
+function createSampleTransactions() {
+  // Create some sample EIP-2718 style transactions (as raw bytes)
+  // These don't need to be valid transactions, just arbitrary byte payloads for testing
+  const transactions = [
+    '0x01' + toHex('sample transaction 1').slice(2),
+    '0x02' + toHex('sample transaction 2').slice(2),
+    '0x02' + toHex('sample transaction 3 with more data').slice(2)
+  ];
+  return transactions;
+}
+
+// EIP-712 helpers for DA Builder
+function createEIP712Domain(chainId, verifyingContract) {
+  return {
+    name: 'TrustlessProposer',
+    version: '1',
+    chainId: BigInt(chainId),
+    verifyingContract: verifyingContract
+  };
+}
+
+function createEIP712Types() {
+  return {
+    Call: [
+      { name: 'deadline', type: 'uint256' },
+      { name: 'nonce', type: 'uint256' },
+      { name: 'target', type: 'address' },
+      { name: 'value', type: 'uint256' },
+      { name: 'calldata', type: 'bytes' },
+      { name: 'gasLimit', type: 'uint256' }
+    ]
+  };
+}
+
+async function prepareDABuilderCall(account, publicClient, proposerAddress, targetAddress, calldata, value, gasLimit) {
+  // Get nonce from TrustlessProposer contract
+  const proposerAbi = parseAbi([
+    'function nestedNonce() view returns (uint256)'
+  ]);
+  
+  const nonce = await publicClient.readContract({
+    address: account.address, // EOA with 7702 code
+    abi: proposerAbi,
+    functionName: 'nestedNonce'
+  });
+  
+  // Set deadline to 5 minutes from now
+  const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
+  
+  // Create EIP-712 message
+  const domain = createEIP712Domain(chain.id, account.address);
+  const types = createEIP712Types();
+  const message = {
+    deadline,
+    nonce,
+    target: targetAddress,
+    value: value || 0n,
+    calldata: calldata || '0x',
+    gasLimit: gasLimit || 500000n
+  };
+  
+  // Sign the message
+  const signature = await account.signTypedData({
+    domain,
+    types,
+    primaryType: 'Call',
+    message
+  });
+  
+  // Encode the onCall parameters
+  const encodedCall = encodeAbiParameters(
+    parseAbiParameters('bytes, uint256, uint256, bytes, uint256'),
+    [signature, deadline, nonce, calldata || '0x', gasLimit || 500000n]
+  );
+  
+  // Encode the onCall function call
+  const onCallData = encodeFunctionData({
+    abi: parseAbi(['function onCall(address target, bytes calldata data, uint256 value) returns (bool)']),
+    functionName: 'onCall',
+    args: [targetAddress, encodedCall, value || 0n]
+  });
+  
+  return onCallData;
+}
+
+async function sendBlobTransaction() {
+  try {
+    if (!PRIVATE_KEY) {
+      throw new Error('PRIVATE_KEY environment variable is required');
+    }
+    
+    const account = privateKeyToAccount(PRIVATE_KEY);
+  
+  const walletClient = createWalletClient({
+    account,
+    chain,
+    transport: http(RPC_URL || chain.rpcUrls.default.http[0])
+  });
+  
+  const publicClient = createPublicClient({
+    chain,
+    transport: http(RPC_URL || chain.rpcUrls.default.http[0])
+  });
+  
+  console.log(`\n📍 Using ${chain.name} network`);
+  console.log(`   RPC: ${RPC_URL || chain.rpcUrls.default.http[0]}`);
+  console.log(`   Account: ${account.address}`);
+  
+  // Get current block and gas fee estimates
+  const currentBlock = await publicClient.getBlockNumber();
+  const block = await publicClient.getBlock({ blockNumber: currentBlock });
+  const targetL1Block = currentBlock + 1n;
+  const feeEst = await publicClient.estimateFeesPerGas()
+  const maxPriorityFeePerGas = feeEst.maxPriorityFeePerGas * 2n
+  const maxFeePerGas = feeEst.maxFeePerGas * 2n
+  // Prefer RPC-provided blob base fee (eth_blobBaseFee)
+  let blobBase = await publicClient.getBlobBaseFee()
+  const maxFeePerBlobGas = (blobBase * 2n) > parseGwei('5') ? (blobBase * 2n) : parseGwei('5');
+
+  console.log(`\n⛽ Gas params:`);
+  console.log(`   maxPriorityFeePerGas: ${formatGwei(maxPriorityFeePerGas)} gwei`);
+  console.log(`   maxFeePerGas:        ${formatGwei(maxFeePerGas)} gwei`);
+  console.log(`   blobBase:            ${formatGwei(blobBase)} gwei`);
+  console.log(`   maxFeePerBlobGas:    ${formatGwei(maxFeePerBlobGas)} gwei`);
+  
+  console.log(`\n📦 Building Facet Batch...`);
+  console.log(`   Version: 1`);
+  console.log(`   L2 Chain ID: ${L2_CHAIN_ID} (0x${L2_CHAIN_ID.toString(16)})`);
+  console.log(`   Role: FORCED (1)`);
+  console.log(`   Target L1 Block: ${targetL1Block}`);
+  
+  // Build spec-compliant batch
+  const transactions = createSampleTransactions();
+  // Set targetL1Block to 0 to avoid strict anchoring for this e2e
+  const batchData = buildFacetBatchData(
+    1,                    // version
+    L2_CHAIN_ID,         // chainId
+    1,                   // role (FORCED)
+    0n,                  // targetL1Block (ignored by parser in this e2e)
+    transactions,        // transactions
+    '0x'                 // extraData
+  );
+  
+  // Compute content hash for verification
+  const contentHash = keccak256(toRlp(batchData));
+  console.log(`   Content Hash: ${contentHash}`);
+  
+  // Create FacetBatch (no signature for FORCED)
+  const batchRlp = encodeFacetBatch(batchData);
+  
+  // Build wire format: magic || uint32_be(length) || rlp(batch)
+  const batchLength = size(batchRlp);
+  const lengthBytes = toHex(batchLength, { size: 4 });
+  const wirePayload = concatHex([FACET_MAGIC_PREFIX, lengthBytes, batchRlp]);
+  
+  console.log(`   Batch RLP Length: ${batchLength} bytes`);
+  console.log(`   Wire Payload Length: ${size(wirePayload)} bytes`);
+  
+  // For DA Builder: send only our data (they handle aggregation)
+  // For direct L1: add filler to simulate aggregation
+  const useDABuilder = !!DA_BUILDER_URL;
+  
+  let dataHex, embedOffset;
+  if (useDABuilder) {
+    // DA Builder will aggregate with other users' data
+    dataHex = wirePayload;
+    embedOffset = 0;  // Unknown until DA Builder aggregates
+  } else {
+    // Simulate aggregation for local testing
+    const fillerBeforeSize = Math.floor(Math.random() * 10000) + 1000;
+    const fillerAfterSize = Math.floor(Math.random() * 10000) + 1000;
+    
+    const fillerBefore = toHex(new Uint8Array(fillerBeforeSize).map(() => Math.floor(Math.random() * 256)));
+    const fillerAfter = toHex(new Uint8Array(fillerAfterSize).map(() => Math.floor(Math.random() * 256)));
+    
+    dataHex = concatHex([fillerBefore, wirePayload, fillerAfter]);
+    embedOffset = fillerBeforeSize;
+  }
+  
+  console.log(`\n🔄 Creating blob...`);
+  console.log(`   Total data size: ${size(dataHex)} bytes`);
+  if (useDABuilder) {
+    console.log(`   Wire payload: ${size(wirePayload)} bytes`);
+    console.log(`   (DA Builder will handle aggregation)`);
+  } else {
+    console.log(`   Embed offset: ${embedOffset} bytes`);
+    console.log(`   Wire payload: ${size(wirePayload)} bytes`);
+    console.log(`   (Added filler for testing)`);
+  }
+  
+  // Create blobs from the data
+  const blobs = toBlobs({ data: dataHex });
+  console.log(`   Created ${blobs.length} blob(s)`);
+  
+  // Set up KZG - explicitly load the trusted setup
+  console.log(`\n🔐 Loading KZG trusted setup`);
+  const trustedSetupPath = process.env.KZG_TRUSTED_SETUP 
+    || process.env.KZG_TRUSTED_SETUP_PATH 
+    || cKzg.DEFAULT_TRUSTED_SETUP_PATH 
+    || mainnetTrustedSetupPath;
+  cKzg.loadTrustedSetup(0, trustedSetupPath);
+  console.log(`   ✓ Trusted setup loaded from: ${trustedSetupPath}`);
+  const kzg = cKzg;
+  
+  if (DA_BUILDER_URL) {
+    console.log('\n🧱 DA Builder mode enabled');
+    console.log(`   Endpoint: ${DA_BUILDER_URL}`);
+    
+    if (!PROPOSER_ADDRESS) {
+      throw new Error('PROPOSER_ADDRESS required for DA Builder mode');
+    }
+    
+    // Check if EOA has EIP-7702 code
+    const eoaCode = await publicClient.getCode({ address: account.address });
+    if (!eoaCode || eoaCode === '0x') {
+      throw new Error(`EOA ${account.address} has no code. Run EIP-7702 setup first.`);
+    }
+    console.log(`   EOA has code (EIP-7702 set): ${eoaCode.slice(0, 10)}...`);
+    
+    // For DA Builder, we need to wrap the blob submission in an onCall
+    // The target will be a contract that accepts blob data, or we can use a dummy target
+    const targetAddress = '0x0000000000000000000000000000000000000000';
+    const calldata = dataHex; // The blob data we want to submit
+    const value = 0n;
+    const gasLimit = 500000n;
+    
+    console.log('   Preparing EIP-712 signed call...');
+    const onCallData = await prepareDABuilderCall(
+      account,
+      publicClient,
+      PROPOSER_ADDRESS,
+      targetAddress,
+      calldata,
+      value,
+      gasLimit
+    );
+    
+    // Create DA Builder client
+    const builderWallet = createWalletClient({ account, chain, transport: http(DA_BUILDER_URL) });
+    const builderPublic = createPublicClient({ transport: http(DA_BUILDER_URL) });
+    
+    // Note: The nestedNonce from TrustlessProposer is used in the EIP-712 signature (in prepareDABuilderCall)
+    // For the L1 transaction nonce, we need a value higher than current nonce to avoid conflicts
+    // DA Builder may not use this directly, but it needs to be valid
+    const currentNonce = await publicClient.getTransactionCount({ address: account.address });
+    const nonce = currentNonce + BigInt(Math.floor(Math.random() * 1000) + 100); // Use a nonce well above current
+    
+    // Option to use eth_sendBundle instead (set USE_SEND_BUNDLE=true in env)
+    const useSendBundle = process.env.USE_SEND_BUNDLE === 'true';
+    
+    let requestId;
+    if (useSendBundle) {
+      console.log('   Using eth_sendBundle method...');
+      
+      // Get current block for target
+      const currentBlock = await publicClient.getBlockNumber();
+      const targetBlock = currentBlock + 1n;
+      
+      // Create and serialize the transaction
+      const tx = await account.signTransaction({
+        to: account.address,
+        data: onCallData,
+        blobs,
+        kzg,
+        nonce,
+        gas: 500000n,
+        maxPriorityFeePerGas,
+        maxFeePerGas,
+        maxFeePerBlobGas,
+        chainId: chain.id,
+        type: 'eip4844'
+      });
+      
+      // Submit via eth_sendBundle
+      // DA Builder's simplified version: accepts object with txs and blockNumber
+      requestId = await builderPublic.request({
+        "jsonrpc": "2.0",
+        "id": 1,
+        method: 'eth_sendBundle',
+        params: [{
+          txs: [tx],  // Array with single serialized transaction
+          blockNumber: toHex(targetBlock)  // Block number as hex string (camelCase)
+        }]
+      });
+      
+      console.log(`   Submitted bundle for block ${targetBlock}`);
+    } else {
+      // Original method using sendTransaction
+      console.log('   Submitting to DA Builder via sendTransaction...');
+      requestId = await builderWallet.sendTransaction({
+        to: account.address, // Send to EOA with 7702 code
+        data: onCallData,
+        blobs,
+        kzg,
+        nonce,  // Explicitly provide nonce
+        gas: 500000n,  // Explicit gas limit
+        maxPriorityFeePerGas,
+        maxFeePerGas,
+        maxFeePerBlobGas,
+      });
+    }
+    
+    console.log(`   Submitted to DA Builder. Request ID: ${requestId}`);
+    console.log('\n⏳ Waiting for DA Builder receipt...');
+    
+    let receipt = null;
+    const startTime = Date.now();
+    const timeout = 900000; // 15 minutes
+    
+    while (!receipt) {
+      try {
+        receipt = await builderPublic.request({ 
+          method: 'eth_getTransactionReceipt', 
+          params: [requestId] 
+        });
+        if (receipt) break;
+      } catch (e) {
+        // Ignore errors, keep polling
+      }
+      
+      if (Date.now() - startTime > timeout) {
+        throw new Error('Timeout waiting for DA Builder receipt');
+      }
+      
+      await new Promise((r) => setTimeout(r, 5000)); // Poll every 5 seconds
+    }
+    
+    console.log(`   ✅ Included on-chain. Tx: ${receipt.transactionHash}`);
+    console.log(`   Block: ${receipt.blockNumber}`);
+    const result = {
+      success: true,
+      mode: 'da_builder',
+      chain: chain.name,
+      requestId,
+      transactionHash: receipt.transactionHash,
+      blockNumber: Number(receipt.blockNumber),
+      embedOffset: embedOffset,
+      wirePayloadLength: size(wirePayload),
+      batchRlpLength: batchLength,
+      contentHash: contentHash,
+      l2ChainId: L2_CHAIN_ID,
+    };
+    console.log('\n--- JSON OUTPUT ---');
+    console.log(JSON.stringify(result, null, 2));
+    return result;
+  } else {
+    console.log('\n🚀 Sending blob transaction directly to L1...');
+    const hash = await walletClient.sendTransaction({
+      to: '0x0000000000000000000000000000000000000000',
+      blobs,
+      kzg,
+      maxPriorityFeePerGas,
+      maxFeePerGas,
+      maxFeePerBlobGas,
+    });
+    console.log(`   Transaction sent: ${hash}`);
+    console.log(`   View on Etherscan: https://${chain.name.toLowerCase()}.etherscan.io/tx/${hash}`);
+    console.log('\n⏳ Waiting for confirmation...');
+    const receipt = await publicClient.waitForTransactionReceipt({ hash });
+    console.log(`   ✅ Confirmed in block ${receipt.blockNumber}`);
+    console.log(`   Blob hashes: ${receipt.blobVersionedHashes?.join(', ') || 'none'}`);
+    const result = {
+      success: true,
+      mode: 'direct',
+      chain: chain.name,
+      transactionHash: hash,
+      blockNumber: Number(receipt.blockNumber),
+      blobVersionedHashes: receipt.blobVersionedHashes || [],
+      embedOffset: embedOffset,
+      wirePayloadLength: size(wirePayload),
+      batchRlpLength: batchLength,
+      contentHash: contentHash,
+      l2ChainId: L2_CHAIN_ID,
+    };
+    console.log('\n--- JSON OUTPUT ---');
+    console.log(JSON.stringify(result, null, 2));
+    return result;
+  }
+  } catch (error) {
+    console.error('❌ Error:', error.message);
+    console.error('Stack trace:', error.stack);
+    const errorResult = {
+      success: false,
+      error: error.message,
+      stack: error.stack
+    };
+    console.log('\n--- JSON OUTPUT ---');
+    console.log(JSON.stringify(errorResult, null, 2));
+    process.exit(1);
+  }
+}
+
+sendBlobTransaction();
diff --git a/sequencer/.dockerignore b/sequencer/.dockerignore
new file mode 100644
index 00000000..93f13619
--- /dev/null
+++ b/sequencer/.dockerignore
@@ -0,0 +1,2 @@
+node_modules
+npm-debug.log
diff --git a/sequencer/.env.example b/sequencer/.env.example
new file mode 100644
index 00000000..31476674
--- /dev/null
+++ b/sequencer/.env.example
@@ -0,0 +1,37 @@
+# L1 Connection
+L1_RPC_URL=https://holesky.infura.io/v3/YOUR_KEY
+L1_CHAIN_ID=17000
+PRIVATE_KEY=0x...
+
+# L2 Connection  
+L2_RPC_URL=http://localhost:8545
+L2_CHAIN_ID=0xface7b
+
+# Facet Configuration
+FACET_MAGIC_PREFIX=0x0000000000012345
+
+# Batching Parameters
+MAX_TX_PER_BATCH=500
+MAX_BATCH_SIZE=131072
+BATCH_INTERVAL_MS=3000
+MAX_PER_SENDER=10
+
+# Economics
+MIN_GAS_PRICE=1000000000
+BASE_FEE_MULTIPLIER=2
+ESCALATION_RATE=1.125
+
+# Operational
+MAX_PENDING_TXS=10000
+DB_PATH=./data/sequencer.db
+PORT=8547
+LOG_LEVEL=info
+
+# Monitoring
+METRICS_ENABLED=true
+METRICS_PORT=9090
+
+# Optional: DA Builder (post-MVP)
+# USE_DA_BUILDER=true
+# DA_BUILDER_URL=https://da-builder.holesky.spire.dev/
+# PROPOSER_ADDRESS=0x...
\ No newline at end of file
diff --git a/sequencer/.gitignore b/sequencer/.gitignore
new file mode 100644
index 00000000..be5da4c9
--- /dev/null
+++ b/sequencer/.gitignore
@@ -0,0 +1,7 @@
+node_modules
+dist
+.env
+data
+.test-nonce
+*.log
+.DS_Store
\ No newline at end of file
diff --git a/sequencer/Dockerfile b/sequencer/Dockerfile
new file mode 100644
index 00000000..45e99d20
--- /dev/null
+++ b/sequencer/Dockerfile
@@ -0,0 +1,47 @@
+FROM node:20-alpine AS builder
+
+WORKDIR /app
+
+# Tooling required to build native modules (better-sqlite3, c-kzg)
+RUN apk add --no-cache python3 make g++
+
+# Install dependencies
+COPY package.json package-lock.json ./
+RUN npm ci
+
+# Copy source and build
+COPY tsconfig.json ./
+COPY eslint.config.mjs ./
+COPY src ./src
+RUN npm run typecheck && npm run build
+
+# Strip dev dependencies so we can reuse node_modules in the runtime image
+RUN npm prune --omit=dev
+
+FROM node:20-alpine
+
+WORKDIR /app
+
+ENV NODE_ENV=production
+
+# Runtime deps for native modules & health checks
+RUN apk add --no-cache libstdc++ curl && \
+    mkdir -p /data
+
+COPY package.json package-lock.json ./
+COPY --from=builder /app/node_modules ./node_modules
+COPY --from=builder /app/dist ./dist
+
+RUN addgroup -g 1001 -S nodejs && \
+    adduser -S nodejs -u 1001 && \
+    chown -R nodejs:nodejs /app /data
+
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+  CMD curl --fail --silent http://localhost:${PORT:-8545}/health || exit 1
+
+EXPOSE 8545 9090
+VOLUME ["/data"]
+
+USER nodejs
+
+CMD ["node", "dist/index.js"]
diff --git a/sequencer/README.md b/sequencer/README.md
new file mode 100644
index 00000000..1e3ac75a
--- /dev/null
+++ b/sequencer/README.md
@@ -0,0 +1,166 @@
+# Facet Micro-Sequencer
+
+A lightweight, permissionless TypeScript sequencer for Facet L2 transactions.
+
+## Features
+
+- **EIP-1559 Transaction Support**: Only accepts properly formatted EIP-1559 transactions
+- **Smart Batching**: Dynamic batch creation based on size, count, and time thresholds
+- **RBF Support**: Automatic fee escalation for stuck transactions
+- **L1/L2 Monitoring**: Tracks transaction inclusion across both layers
+- **Reorg Handling**: Automatically detects and handles L1 reorgs
+- **SQLite Storage**: Simple, embedded database with WAL mode for performance
+- **Prometheus Metrics**: Built-in metrics endpoint for monitoring
+
+## Quick Start
+
+### Development
+
+```bash
+# Install dependencies
+npm install
+
+# Copy and configure environment
+cp .env.example .env
+# Edit .env with your configuration
+
+# Run in development mode
+npm run dev
+```
+
+### Production
+
+```bash
+# Build
+npm run build
+
+# Run
+npm start
+```
+
+### Docker
+
+```bash
+# Build image
+docker build -t facet-sequencer .
+
+# Run container
+docker run -d \
+  --name facet-sequencer \
+  -p 8547:8547 \
+  -p 9090:9090 \
+  -v $(pwd)/data:/data \
+  -v $(pwd)/.env:/app/.env:ro \
+  facet-sequencer
+```
+
+## Configuration
+
+All configuration is done via environment variables:
+
+```env
+# L1 Connection (required)
+L1_RPC_URL=https://holesky.infura.io/v3/YOUR_KEY
+PRIVATE_KEY=0x...  # Private key for L1 transactions
+
+# L2 Connection
+L2_RPC_URL=http://localhost:8545  # Your Facet node RPC
+
+# Batching
+MAX_TX_PER_BATCH=500
+BATCH_INTERVAL_MS=3000  # Create batch every 3 seconds if transactions pending
+
+# Economics
+MIN_GAS_PRICE=1000000000  # 1 gwei minimum
+```
+
+## API Endpoints
+
+### JSON-RPC
+
+- `eth_sendRawTransaction` - Submit a transaction
+- `eth_chainId` - Get the L2 chain ID
+- `sequencer_getTxStatus` - Get detailed transaction status
+- `sequencer_getStats` - Get sequencer statistics
+
+### HTTP
+
+- `GET /health` - Health check endpoint
+- `GET /metrics` - Prometheus metrics
+
+## Transaction Lifecycle
+
+1. **Queued**: Transaction received and validated
+2. **Batched**: Included in a batch
+3. **Submitted**: Batch sent to L1
+4. **L1 Included**: Batch confirmed on L1
+5. **L2 Included**: Transaction executed on L2
+
+## Database Schema
+
+The sequencer uses SQLite with the following main tables:
+
+- `transactions`: Transaction pool and state tracking
+- `batches`: Batch creation and L1 submission tracking
+- `batch_items`: Transaction ordering within batches
+- `post_attempts`: L1 submission attempts with RBF chain
+
+## Monitoring
+
+The sequencer exposes Prometheus metrics on port 9090:
+
+- `sequencer_queued_txs`: Current queued transactions
+- `sequencer_included_txs_total`: Total included transactions
+- `sequencer_confirmed_batches_total`: Total L1 confirmed batches
+- `sequencer_pending_batches`: Current pending batches
+
+## Development
+
+```bash
+# Run tests
+npm test
+
+# Type checking
+npm run typecheck
+
+# Linting
+npm run lint
+
+# Database migrations
+npm run migrate
+```
+
+## Architecture
+
+```
+┌─────────────┐  eth_sendRawTransaction
+│  HTTP RPC   │◄──────── users
+└────┬────────┘
+     ▼
+┌─────────────┐  
+│   Ingress   │  validates and stores
+└────┬────────┘
+     ▼
+┌─────────────┐  
+│ BatchMaker  │  creates Facet batches
+└────┬────────┘
+     ▼
+┌─────────────┐  
+│   Poster    │  submits to L1 with RBF
+└────┬────────┘
+     ▼
+┌──────────────┐
+│   Monitor    │  tracks inclusion
+└──────────────┘
+```
+
+## Security
+
+- Private keys are never logged
+- Transactions are validated before acceptance
+- Sender fairness prevents monopolization
+- Database uses WAL mode for consistency
+
+## License
+
+MIT
\ No newline at end of file
diff --git a/sequencer/eslint.config.mjs b/sequencer/eslint.config.mjs
new file mode 100644
index 00000000..5aae70d0
--- /dev/null
+++ b/sequencer/eslint.config.mjs
@@ -0,0 +1,41 @@
+import { defineConfig } from "eslint/config";
+import globals from "globals";
+import tsParser from "@typescript-eslint/parser";
+import path from "node:path";
+import { fileURLToPath } from "node:url";
+import js from "@eslint/js";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = path.dirname(__filename);
+const compat = new FlatCompat({
+    baseDirectory: __dirname,
+    recommendedConfig: js.configs.recommended,
+    allConfig: js.configs.all
+});
+
+export default defineConfig([{
+    extends: compat.extends("eslint:recommended", "plugin:@typescript-eslint/recommended"),
+
+    languageOptions: {
+        globals: {
+            ...globals.node,
+        },
+
+        parser: tsParser,
+        ecmaVersion: 2022,
+        sourceType: "module",
+    },
+
+    rules: {
+        "@typescript-eslint/no-explicit-any": "off",
+        "@typescript-eslint/no-unused-vars": "off",
+        "@typescript-eslint/ban-ts-comment": "off",
+        "@typescript-eslint/no-non-null-assertion": "off",
+        "no-unused-vars": "off",
+        "no-case-declarations": "off",
+        "no-empty": "warn",
+        "no-unreachable": "error",
+        "no-undef": "off",
+    },
+}]);
\ No newline at end of file
diff --git a/sequencer/package-lock.json b/sequencer/package-lock.json
new file mode 100644
index 00000000..be4331e1
--- /dev/null
+++ b/sequencer/package-lock.json
@@ -0,0 +1,4690 @@
+{
+  "name": "@facet/sequencer",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "@facet/sequencer",
+      "version": "0.1.0",
+      "dependencies": {
+        "@fastify/cors": "11.1.0",
+        "better-sqlite3": "12.4.1",
+        "c-kzg": "4.1.0",
+        "dotenv": "17.2.2",
+        "fastify": "5.6.0",
+        "p-queue": "8.1.1",
+        "pino": "9.9.5",
+        "pino-pretty": "13.1.1",
+        "viem": "2.37.5"
+      },
+      "devDependencies": {
+        "@types/better-sqlite3": "7.6.13",
+        "@types/node": "^20.11.0",
+        "@typescript-eslint/eslint-plugin": "8.43.0",
+        "@typescript-eslint/parser": "8.43.0",
+        "eslint": "9.35.0",
+        "tsx": "4.20.5",
+        "typescript": "5.9.2",
+        "vitest": "3.2.4"
+      },
+      "engines": {
+        "node": ">=20.0.0"
+      }
+    },
+    "node_modules/@adraffy/ens-normalize": {
+      "version": "1.11.0",
+      "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz",
+      "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==",
+      "license": "MIT"
+    },
+    "node_modules/@esbuild/aix-ppc64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
+      "integrity": "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "aix"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.9.tgz",
+      "integrity": "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.9.tgz",
+      "integrity": "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/android-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.9.tgz",
+      "integrity": "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.9.tgz",
+      "integrity": "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/darwin-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.9.tgz",
+      "integrity": "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/freebsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.9.tgz",
+      "integrity": "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.9.tgz",
+      "integrity": "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.9.tgz",
+      "integrity": "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ia32": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.9.tgz",
+      "integrity": "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-loong64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.9.tgz",
+      "integrity": "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-mips64el": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.9.tgz",
+      "integrity": "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA==",
+      "cpu": [
+        "mips64el"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-ppc64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.9.tgz",
+      "integrity": "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-riscv64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.9.tgz",
+      "integrity": "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-s390x": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.9.tgz",
+      "integrity": "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/linux-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.9.tgz",
+      "integrity": "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/netbsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.9.tgz",
+      "integrity": "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "netbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.9.tgz",
+      "integrity": "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openbsd-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.9.tgz",
+      "integrity": "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openbsd"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/openharmony-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.9.tgz",
+      "integrity": "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/sunos-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.9.tgz",
+      "integrity": "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "sunos"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-arm64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.9.tgz",
+      "integrity": "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-ia32": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.9.tgz",
+      "integrity": "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@esbuild/win32-x64": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.9.tgz",
+      "integrity": "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ],
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/@eslint-community/eslint-utils": {
+      "version": "4.9.0",
+      "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
+      "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eslint-visitor-keys": "^3.4.3"
+      },
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+      }
+    },
+    "node_modules/@eslint-community/regexpp": {
+      "version": "4.12.1",
+      "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz",
+      "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+      }
+    },
+    "node_modules/@eslint/config-array": {
+      "version": "0.21.0",
+      "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
+      "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/object-schema": "^2.1.6",
+        "debug": "^4.3.1",
+        "minimatch": "^3.1.2"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/config-array/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@eslint/config-array/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@eslint/config-helpers": {
+      "version": "0.3.1",
+      "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
+      "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/core": {
+      "version": "0.15.2",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
+      "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/eslintrc": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz",
+      "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^6.12.4",
+        "debug": "^4.3.2",
+        "espree": "^10.0.1",
+        "globals": "^14.0.0",
+        "ignore": "^5.2.0",
+        "import-fresh": "^3.2.1",
+        "js-yaml": "^4.1.0",
+        "minimatch": "^3.1.2",
+        "strip-json-comments": "^3.1.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/@eslint/js": {
+      "version": "9.35.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.35.0.tgz",
+      "integrity": "sha512-30iXE9whjlILfWobBkNerJo+TXYsgVM5ERQwMcMKCHckHflCmf7wXDAHlARoWnh0s1U72WqlbeyE7iAcCzuCPw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      }
+    },
+    "node_modules/@eslint/object-schema": {
+      "version": "2.1.6",
+      "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz",
+      "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@eslint/plugin-kit": {
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
+      "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@eslint/core": "^0.15.2",
+        "levn": "^0.4.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
+    "node_modules/@fastify/ajv-compiler": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.2.tgz",
+      "integrity": "sha512-Rkiu/8wIjpsf46Rr+Fitd3HRP+VsxUFDDeag0hs9L0ksfnwx2g7SPQQTFL0E8Qv+rfXzQOxBJnjUB9ITUDjfWQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.12.0",
+        "ajv-formats": "^3.0.1",
+        "fast-uri": "^3.0.0"
+      }
+    },
+    "node_modules/@fastify/ajv-compiler/node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "license": "MIT"
+    },
+    "node_modules/@fastify/cors": {
+      "version": "11.1.0",
+      "resolved": "https://registry.npmjs.org/@fastify/cors/-/cors-11.1.0.tgz",
+      "integrity": "sha512-sUw8ed8wP2SouWZTIbA7V2OQtMNpLj2W6qJOYhNdcmINTu6gsxVYXjQiM9mdi8UUDlcoDDJ/W2syPo1WB2QjYA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "fastify-plugin": "^5.0.0",
+        "toad-cache": "^3.7.0"
+      }
+    },
+    "node_modules/@fastify/error": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz",
+      "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/@fastify/fast-json-stringify-compiler": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz",
+      "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "fast-json-stringify": "^6.0.0"
+      }
+    },
+    "node_modules/@fastify/forwarded": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.0.tgz",
+      "integrity": "sha512-kJExsp4JCms7ipzg7SJ3y8DwmePaELHxKYtg+tZow+k0znUTf3cb+npgyqm8+ATZOdmfgfydIebPDWM172wfyA==",
+      "license": "MIT"
+    },
+    "node_modules/@fastify/merge-json-schemas": {
+      "version": "0.2.1",
+      "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz",
+      "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "dequal": "^2.0.3"
+      }
+    },
+    "node_modules/@fastify/proxy-addr": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.0.0.tgz",
+      "integrity": "sha512-37qVVA1qZ5sgH7KpHkkC4z9SK6StIsIcOmpjvMPXNb3vx2GQxhZocogVYbr2PbbeLCQxYIPDok307xEvRZOzGA==",
+      "license": "MIT",
+      "dependencies": {
+        "@fastify/forwarded": "^3.0.0",
+        "ipaddr.js": "^2.1.0"
+      }
+    },
+    "node_modules/@humanfs/core": {
+      "version": "0.19.1",
+      "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
+      "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanfs/node": {
+      "version": "0.16.7",
+      "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
+      "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@humanfs/core": "^0.19.1",
+        "@humanwhocodes/retry": "^0.4.0"
+      },
+      "engines": {
+        "node": ">=18.18.0"
+      }
+    },
+    "node_modules/@humanwhocodes/module-importer": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+      "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.22"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@humanwhocodes/retry": {
+      "version": "0.4.3",
+      "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+      "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=18.18"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/nzakas"
+      }
+    },
+    "node_modules/@jridgewell/sourcemap-codec": {
+      "version": "1.5.5",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+      "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@noble/ciphers": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz",
+      "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/curves": {
+      "version": "1.9.1",
+      "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz",
+      "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "1.8.0"
+      },
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@noble/hashes": {
+      "version": "1.8.0",
+      "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz",
+      "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^14.21.3 || >=16"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@nodelib/fs.scandir": {
+      "version": "2.1.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
+      "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "2.0.5",
+        "run-parallel": "^1.1.9"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.stat": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz",
+      "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@nodelib/fs.walk": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz",
+      "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.scandir": "2.1.5",
+        "fastq": "^1.6.0"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/@rollup/rollup-android-arm-eabi": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.50.1.tgz",
+      "integrity": "sha512-HJXwzoZN4eYTdD8bVV22DN8gsPCAj3V20NHKOs8ezfXanGpmVPR7kalUHd+Y31IJp9stdB87VKPFbsGY3H/2ag==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-android-arm64": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.50.1.tgz",
+      "integrity": "sha512-PZlsJVcjHfcH53mOImyt3bc97Ep3FJDXRpk9sMdGX0qgLmY0EIWxCag6EigerGhLVuL8lDVYNnSo8qnTElO4xw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "android"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-arm64": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.50.1.tgz",
+      "integrity": "sha512-xc6i2AuWh++oGi4ylOFPmzJOEeAa2lJeGUGb4MudOtgfyyjr4UPNK+eEWTPLvmPJIY/pgw6ssFIox23SyrkkJw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-darwin-x64": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.50.1.tgz",
+      "integrity": "sha512-2ofU89lEpDYhdLAbRdeyz/kX3Y2lpYc6ShRnDjY35bZhd2ipuDMDi6ZTQ9NIag94K28nFMofdnKeHR7BT0CATw==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.50.1.tgz",
+      "integrity": "sha512-wOsE6H2u6PxsHY/BeFHA4VGQN3KUJFZp7QJBmDYI983fgxq5Th8FDkVuERb2l9vDMs1D5XhOrhBrnqcEY6l8ZA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.50.1.tgz",
+      "integrity": "sha512-A/xeqaHTlKbQggxCqispFAcNjycpUEHP52mwMQZUNqDUJFFYtPHCXS1VAG29uMlDzIVr+i00tSFWFLivMcoIBQ==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.50.1.tgz",
+      "integrity": "sha512-54v4okehwl5TaSIkpp97rAHGp7t3ghinRd/vyC1iXqXMfjYUTm7TfYmCzXDoHUPTTf36L8pr0E7YsD3CfB3ZDg==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.50.1.tgz",
+      "integrity": "sha512-p/LaFyajPN/0PUHjv8TNyxLiA7RwmDoVY3flXHPSzqrGcIp/c2FjwPPP5++u87DGHtw+5kSH5bCJz0mvXngYxw==",
+      "cpu": [
+        "arm"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.50.1.tgz",
+      "integrity": "sha512-2AbMhFFkTo6Ptna1zO7kAXXDLi7H9fGTbVaIq2AAYO7yzcAsuTNWPHhb2aTA6GPiP+JXh85Y8CiS54iZoj4opw==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-arm64-musl": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.50.1.tgz",
+      "integrity": "sha512-Cgef+5aZwuvesQNw9eX7g19FfKX5/pQRIyhoXLCiBOrWopjo7ycfB292TX9MDcDijiuIJlx1IzJz3IoCPfqs9w==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.50.1.tgz",
+      "integrity": "sha512-RPhTwWMzpYYrHrJAS7CmpdtHNKtt2Ueo+BlLBjfZEhYBhK00OsEqM08/7f+eohiF6poe0YRDDd8nAvwtE/Y62Q==",
+      "cpu": [
+        "loong64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.50.1.tgz",
+      "integrity": "sha512-eSGMVQw9iekut62O7eBdbiccRguuDgiPMsw++BVUg+1K7WjZXHOg/YOT9SWMzPZA+w98G+Fa1VqJgHZOHHnY0Q==",
+      "cpu": [
+        "ppc64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.50.1.tgz",
+      "integrity": "sha512-S208ojx8a4ciIPrLgazF6AgdcNJzQE4+S9rsmOmDJkusvctii+ZvEuIC4v/xFqzbuP8yDjn73oBlNDgF6YGSXQ==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-riscv64-musl": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.50.1.tgz",
+      "integrity": "sha512-3Ag8Ls1ggqkGUvSZWYcdgFwriy2lWo+0QlYgEFra/5JGtAd6C5Hw59oojx1DeqcA2Wds2ayRgvJ4qxVTzCHgzg==",
+      "cpu": [
+        "riscv64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.50.1.tgz",
+      "integrity": "sha512-t9YrKfaxCYe7l7ldFERE1BRg/4TATxIg+YieHQ966jwvo7ddHJxPj9cNFWLAzhkVsbBvNA4qTbPVNsZKBO4NSg==",
+      "cpu": [
+        "s390x"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-gnu": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.1.tgz",
+      "integrity": "sha512-MCgtFB2+SVNuQmmjHf+wfI4CMxy3Tk8XjA5Z//A0AKD7QXUYFMQcns91K6dEHBvZPCnhJSyDWLApk40Iq/H3tA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-linux-x64-musl": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.50.1.tgz",
+      "integrity": "sha512-nEvqG+0jeRmqaUMuwzlfMKwcIVffy/9KGbAGyoa26iu6eSngAYQ512bMXuqqPrlTyfqdlB9FVINs93j534UJrg==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "linux"
+      ]
+    },
+    "node_modules/@rollup/rollup-openharmony-arm64": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.50.1.tgz",
+      "integrity": "sha512-RDsLm+phmT3MJd9SNxA9MNuEAO/J2fhW8GXk62G/B4G7sLVumNFbRwDL6v5NrESb48k+QMqdGbHgEtfU0LCpbA==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "openharmony"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.50.1.tgz",
+      "integrity": "sha512-hpZB/TImk2FlAFAIsoElM3tLzq57uxnGYwplg6WDyAxbYczSi8O2eQ+H2Lx74504rwKtZ3N2g4bCUkiamzS6TQ==",
+      "cpu": [
+        "arm64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.50.1.tgz",
+      "integrity": "sha512-SXjv8JlbzKM0fTJidX4eVsH+Wmnp0/WcD8gJxIZyR6Gay5Qcsmdbi9zVtnbkGPG8v2vMR1AD06lGWy5FLMcG7A==",
+      "cpu": [
+        "ia32"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@rollup/rollup-win32-x64-msvc": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.50.1.tgz",
+      "integrity": "sha512-StxAO/8ts62KZVRAm4JZYq9+NqNsV7RvimNK+YM7ry//zebEH6meuugqW/P5OFUCjyQgui+9fUxT6d5NShvMvA==",
+      "cpu": [
+        "x64"
+      ],
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "win32"
+      ]
+    },
+    "node_modules/@scure/base": {
+      "version": "1.2.6",
+      "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz",
+      "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip32": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz",
+      "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "~1.9.0",
+        "@noble/hashes": "~1.8.0",
+        "@scure/base": "~1.2.5"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@scure/bip39": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz",
+      "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==",
+      "license": "MIT",
+      "dependencies": {
+        "@noble/hashes": "~1.8.0",
+        "@scure/base": "~1.2.5"
+      },
+      "funding": {
+        "url": "https://paulmillr.com/funding/"
+      }
+    },
+    "node_modules/@types/better-sqlite3": {
+      "version": "7.6.13",
+      "resolved": "https://registry.npmjs.org/@types/better-sqlite3/-/better-sqlite3-7.6.13.tgz",
+      "integrity": "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*"
+      }
+    },
+    "node_modules/@types/chai": {
+      "version": "5.2.2",
+      "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz",
+      "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/deep-eql": "*"
+      }
+    },
+    "node_modules/@types/deep-eql": {
+      "version": "4.0.2",
+      "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+      "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/estree": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+      "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/json-schema": {
+      "version": "7.0.15",
+      "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
+      "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@types/node": {
+      "version": "20.19.13",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.13.tgz",
+      "integrity": "sha512-yCAeZl7a0DxgNVteXFHt9+uyFbqXGy/ShC4BlcHkoE0AfGXYv/BUiplV72DjMYXHDBXFjhvr6DD1NiRVfB4j8g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~6.21.0"
+      }
+    },
+    "node_modules/@typescript-eslint/eslint-plugin": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.43.0.tgz",
+      "integrity": "sha512-8tg+gt7ENL7KewsKMKDHXR1vm8tt9eMxjJBYINf6swonlWgkYn5NwyIgXpbbDxTNU5DgpDFfj95prcTq2clIQQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/regexpp": "^4.10.0",
+        "@typescript-eslint/scope-manager": "8.43.0",
+        "@typescript-eslint/type-utils": "8.43.0",
+        "@typescript-eslint/utils": "8.43.0",
+        "@typescript-eslint/visitor-keys": "8.43.0",
+        "graphemer": "^1.4.0",
+        "ignore": "^7.0.0",
+        "natural-compare": "^1.4.0",
+        "ts-api-utils": "^2.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "@typescript-eslint/parser": "^8.43.0",
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/parser": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.43.0.tgz",
+      "integrity": "sha512-B7RIQiTsCBBmY+yW4+ILd6mF5h1FUwJsVvpqkrgpszYifetQ2Ke+Z4u6aZh0CblkUGIdR59iYVyXqqZGkZ3aBw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/scope-manager": "8.43.0",
+        "@typescript-eslint/types": "8.43.0",
+        "@typescript-eslint/typescript-estree": "8.43.0",
+        "@typescript-eslint/visitor-keys": "8.43.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/project-service": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.43.0.tgz",
+      "integrity": "sha512-htB/+D/BIGoNTQYffZw4uM4NzzuolCoaA/BusuSIcC8YjmBYQioew5VUZAYdAETPjeed0hqCaW7EHg+Robq8uw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/tsconfig-utils": "^8.43.0",
+        "@typescript-eslint/types": "^8.43.0",
+        "debug": "^4.3.4"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/scope-manager": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.43.0.tgz",
+      "integrity": "sha512-daSWlQ87ZhsjrbMLvpuuMAt3y4ba57AuvadcR7f3nl8eS3BjRc8L9VLxFLk92RL5xdXOg6IQ+qKjjqNEimGuAg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.43.0",
+        "@typescript-eslint/visitor-keys": "8.43.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/tsconfig-utils": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.43.0.tgz",
+      "integrity": "sha512-ALC2prjZcj2YqqL5X/bwWQmHA2em6/94GcbB/KKu5SX3EBDOsqztmmX1kMkvAJHzxk7TazKzJfFiEIagNV3qEA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/type-utils": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.43.0.tgz",
+      "integrity": "sha512-qaH1uLBpBuBBuRf8c1mLJ6swOfzCXryhKND04Igr4pckzSEW9JX5Aw9AgW00kwfjWJF0kk0ps9ExKTfvXfw4Qg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.43.0",
+        "@typescript-eslint/typescript-estree": "8.43.0",
+        "@typescript-eslint/utils": "8.43.0",
+        "debug": "^4.3.4",
+        "ts-api-utils": "^2.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/types": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.43.0.tgz",
+      "integrity": "sha512-vQ2FZaxJpydjSZJKiSW/LJsabFFvV7KgLC5DiLhkBcykhQj8iK9BOaDmQt74nnKdLvceM5xmhaTF+pLekrxEkw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/typescript-estree": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.43.0.tgz",
+      "integrity": "sha512-7Vv6zlAhPb+cvEpP06WXXy/ZByph9iL6BQRBDj4kmBsW98AqEeQHlj/13X+sZOrKSo9/rNKH4Ul4f6EICREFdw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/project-service": "8.43.0",
+        "@typescript-eslint/tsconfig-utils": "8.43.0",
+        "@typescript-eslint/types": "8.43.0",
+        "@typescript-eslint/visitor-keys": "8.43.0",
+        "debug": "^4.3.4",
+        "fast-glob": "^3.3.2",
+        "is-glob": "^4.0.3",
+        "minimatch": "^9.0.4",
+        "semver": "^7.6.0",
+        "ts-api-utils": "^2.1.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/utils": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.43.0.tgz",
+      "integrity": "sha512-S1/tEmkUeeswxd0GGcnwuVQPFWo8NzZTOMxCvw8BX7OMxnNae+i8Tm7REQen/SwUIPoPqfKn7EaZ+YLpiB3k9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.7.0",
+        "@typescript-eslint/scope-manager": "8.43.0",
+        "@typescript-eslint/types": "8.43.0",
+        "@typescript-eslint/typescript-estree": "8.43.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      },
+      "peerDependencies": {
+        "eslint": "^8.57.0 || ^9.0.0",
+        "typescript": ">=4.8.4 <6.0.0"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys": {
+      "version": "8.43.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.43.0.tgz",
+      "integrity": "sha512-T+S1KqRD4sg/bHfLwrpF/K3gQLBM1n7Rp7OjjikjTEssI2YJzQpi5WXoynOaQ93ERIuq3O8RBTOUYDKszUCEHw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@typescript-eslint/types": "8.43.0",
+        "eslint-visitor-keys": "^4.2.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "type": "opencollective",
+        "url": "https://opencollective.com/typescript-eslint"
+      }
+    },
+    "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/@vitest/expect": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz",
+      "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/chai": "^5.2.2",
+        "@vitest/spy": "3.2.4",
+        "@vitest/utils": "3.2.4",
+        "chai": "^5.2.0",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/mocker": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz",
+      "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/spy": "3.2.4",
+        "estree-walker": "^3.0.3",
+        "magic-string": "^0.30.17"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "msw": "^2.4.9",
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+      },
+      "peerDependenciesMeta": {
+        "msw": {
+          "optional": true
+        },
+        "vite": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/@vitest/pretty-format": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz",
+      "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/runner": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz",
+      "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/utils": "3.2.4",
+        "pathe": "^2.0.3",
+        "strip-literal": "^3.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/snapshot": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz",
+      "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.2.4",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/spy": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz",
+      "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "tinyspy": "^4.0.3"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/@vitest/utils": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz",
+      "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@vitest/pretty-format": "3.2.4",
+        "loupe": "^3.1.4",
+        "tinyrainbow": "^2.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/abitype": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.1.0.tgz",
+      "integrity": "sha512-6Vh4HcRxNMLA0puzPjM5GBgT4aAcFGKZzSgAXvuZ27shJP6NEpielTuqbBmZILR5/xd0PizkBGy5hReKz9jl5A==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/wevm"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4",
+        "zod": "^3.22.0 || ^4.0.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        },
+        "zod": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/abstract-logging": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz",
+      "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==",
+      "license": "MIT"
+    },
+    "node_modules/acorn": {
+      "version": "8.15.0",
+      "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
+      "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
+      "dev": true,
+      "license": "MIT",
+      "bin": {
+        "acorn": "bin/acorn"
+      },
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/acorn-jsx": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+      "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+      "dev": true,
+      "license": "MIT",
+      "peerDependencies": {
+        "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+      }
+    },
+    "node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz",
+      "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependencies": {
+        "ajv": "^8.0.0"
+      },
+      "peerDependenciesMeta": {
+        "ajv": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/ajv-formats/node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/ajv-formats/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "license": "MIT"
+    },
+    "node_modules/ansi-styles": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+      "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-convert": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/argparse": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
+      "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+      "dev": true,
+      "license": "Python-2.0"
+    },
+    "node_modules/assertion-error": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+      "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/atomic-sleep": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
+      "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.0.0"
+      }
+    },
+    "node_modules/avvio": {
+      "version": "9.1.0",
+      "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.1.0.tgz",
+      "integrity": "sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==",
+      "license": "MIT",
+      "dependencies": {
+        "@fastify/error": "^4.0.0",
+        "fastq": "^1.17.1"
+      }
+    },
+    "node_modules/balanced-match": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+      "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/base64-js": {
+      "version": "1.5.1",
+      "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+      "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/better-sqlite3": {
+      "version": "12.4.1",
+      "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-12.4.1.tgz",
+      "integrity": "sha512-3yVdyZhklTiNrtg+4WqHpJpFDd+WHTg2oM7UcR80GqL05AOV0xEJzc6qNvFYoEtE+hRp1n9MpN6/+4yhlGkDXQ==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "bindings": "^1.5.0",
+        "prebuild-install": "^7.1.1"
+      },
+      "engines": {
+        "node": "20.x || 22.x || 23.x || 24.x"
+      }
+    },
+    "node_modules/bindings": {
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
+      "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
+      "license": "MIT",
+      "dependencies": {
+        "file-uri-to-path": "1.0.0"
+      }
+    },
+    "node_modules/bl": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz",
+      "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==",
+      "license": "MIT",
+      "dependencies": {
+        "buffer": "^5.5.0",
+        "inherits": "^2.0.4",
+        "readable-stream": "^3.4.0"
+      }
+    },
+    "node_modules/bl/node_modules/buffer": {
+      "version": "5.7.1",
+      "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+      "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "base64-js": "^1.3.1",
+        "ieee754": "^1.1.13"
+      }
+    },
+    "node_modules/bl/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/braces": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
+      "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fill-range": "^7.1.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/c-kzg": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/c-kzg/-/c-kzg-4.1.0.tgz",
+      "integrity": "sha512-eliOBB2GKoT5Nk4LwN418O8kWfXCwepHj3kd6z0zKrzIdJbry0Y8IDPYzE5Dxw/fs386PGO6zQRqy8LSVtR5tQ==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "dependencies": {
+        "bindings": "^1.5.0",
+        "node-addon-api": "^8.3.1"
+      }
+    },
+    "node_modules/cac": {
+      "version": "6.7.14",
+      "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz",
+      "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/callsites": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
+      "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/chai": {
+      "version": "5.3.3",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
+      "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "assertion-error": "^2.0.1",
+        "check-error": "^2.1.1",
+        "deep-eql": "^5.0.1",
+        "loupe": "^3.1.0",
+        "pathval": "^2.0.0"
+      },
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/chalk": {
+      "version": "4.1.2",
+      "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+      "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.1.0",
+        "supports-color": "^7.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/chalk?sponsor=1"
+      }
+    },
+    "node_modules/check-error": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz",
+      "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 16"
+      }
+    },
+    "node_modules/chownr": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
+      "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
+      "license": "ISC"
+    },
+    "node_modules/color-convert": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+      "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "color-name": "~1.1.4"
+      },
+      "engines": {
+        "node": ">=7.0.0"
+      }
+    },
+    "node_modules/color-name": {
+      "version": "1.1.4",
+      "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+      "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/colorette": {
+      "version": "2.0.20",
+      "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
+      "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==",
+      "license": "MIT"
+    },
+    "node_modules/concat-map": {
+      "version": "0.0.1",
+      "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+      "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/cookie": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz",
+      "integrity": "sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
+    },
+    "node_modules/cross-spawn": {
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "path-key": "^3.1.0",
+        "shebang-command": "^2.0.0",
+        "which": "^2.0.1"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/dateformat": {
+      "version": "4.6.3",
+      "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz",
+      "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==",
+      "license": "MIT",
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/debug": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
+      "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.1.3"
+      },
+      "engines": {
+        "node": ">=6.0"
+      },
+      "peerDependenciesMeta": {
+        "supports-color": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/decompress-response": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
+      "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "mimic-response": "^3.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/deep-eql": {
+      "version": "5.0.2",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz",
+      "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/deep-extend": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
+      "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=4.0.0"
+      }
+    },
+    "node_modules/deep-is": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
+      "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/dequal": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+      "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/detect-libc": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
+      "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/dotenv": {
+      "version": "17.2.2",
+      "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.2.tgz",
+      "integrity": "sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==",
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://dotenvx.com"
+      }
+    },
+    "node_modules/end-of-stream": {
+      "version": "1.4.5",
+      "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
+      "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==",
+      "license": "MIT",
+      "dependencies": {
+        "once": "^1.4.0"
+      }
+    },
+    "node_modules/es-module-lexer": {
+      "version": "1.7.0",
+      "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
+      "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/esbuild": {
+      "version": "0.25.9",
+      "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.9.tgz",
+      "integrity": "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "esbuild": "bin/esbuild"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "optionalDependencies": {
+        "@esbuild/aix-ppc64": "0.25.9",
+        "@esbuild/android-arm": "0.25.9",
+        "@esbuild/android-arm64": "0.25.9",
+        "@esbuild/android-x64": "0.25.9",
+        "@esbuild/darwin-arm64": "0.25.9",
+        "@esbuild/darwin-x64": "0.25.9",
+        "@esbuild/freebsd-arm64": "0.25.9",
+        "@esbuild/freebsd-x64": "0.25.9",
+        "@esbuild/linux-arm": "0.25.9",
+        "@esbuild/linux-arm64": "0.25.9",
+        "@esbuild/linux-ia32": "0.25.9",
+        "@esbuild/linux-loong64": "0.25.9",
+        "@esbuild/linux-mips64el": "0.25.9",
+        "@esbuild/linux-ppc64": "0.25.9",
+        "@esbuild/linux-riscv64": "0.25.9",
+        "@esbuild/linux-s390x": "0.25.9",
+        "@esbuild/linux-x64": "0.25.9",
+        "@esbuild/netbsd-arm64": "0.25.9",
+        "@esbuild/netbsd-x64": "0.25.9",
+        "@esbuild/openbsd-arm64": "0.25.9",
+        "@esbuild/openbsd-x64": "0.25.9",
+        "@esbuild/openharmony-arm64": "0.25.9",
+        "@esbuild/sunos-x64": "0.25.9",
+        "@esbuild/win32-arm64": "0.25.9",
+        "@esbuild/win32-ia32": "0.25.9",
+        "@esbuild/win32-x64": "0.25.9"
+      }
+    },
+    "node_modules/escape-string-regexp": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+      "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/eslint": {
+      "version": "9.35.0",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.35.0.tgz",
+      "integrity": "sha512-QePbBFMJFjgmlE+cXAlbHZbHpdFVS2E/6vzCy7aKlebddvl1vadiC4JFV5u/wqTkNUwEV8WrQi257jf5f06hrg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@eslint-community/eslint-utils": "^4.8.0",
+        "@eslint-community/regexpp": "^4.12.1",
+        "@eslint/config-array": "^0.21.0",
+        "@eslint/config-helpers": "^0.3.1",
+        "@eslint/core": "^0.15.2",
+        "@eslint/eslintrc": "^3.3.1",
+        "@eslint/js": "9.35.0",
+        "@eslint/plugin-kit": "^0.3.5",
+        "@humanfs/node": "^0.16.6",
+        "@humanwhocodes/module-importer": "^1.0.1",
+        "@humanwhocodes/retry": "^0.4.2",
+        "@types/estree": "^1.0.6",
+        "@types/json-schema": "^7.0.15",
+        "ajv": "^6.12.4",
+        "chalk": "^4.0.0",
+        "cross-spawn": "^7.0.6",
+        "debug": "^4.3.2",
+        "escape-string-regexp": "^4.0.0",
+        "eslint-scope": "^8.4.0",
+        "eslint-visitor-keys": "^4.2.1",
+        "espree": "^10.4.0",
+        "esquery": "^1.5.0",
+        "esutils": "^2.0.2",
+        "fast-deep-equal": "^3.1.3",
+        "file-entry-cache": "^8.0.0",
+        "find-up": "^5.0.0",
+        "glob-parent": "^6.0.2",
+        "ignore": "^5.2.0",
+        "imurmurhash": "^0.1.4",
+        "is-glob": "^4.0.0",
+        "json-stable-stringify-without-jsonify": "^1.0.1",
+        "lodash.merge": "^4.6.2",
+        "minimatch": "^3.1.2",
+        "natural-compare": "^1.4.0",
+        "optionator": "^0.9.3"
+      },
+      "bin": {
+        "eslint": "bin/eslint.js"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://eslint.org/donate"
+      },
+      "peerDependencies": {
+        "jiti": "*"
+      },
+      "peerDependenciesMeta": {
+        "jiti": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/eslint-scope": {
+      "version": "8.4.0",
+      "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
+      "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "esrecurse": "^4.3.0",
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint-visitor-keys": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+      "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/brace-expansion": {
+      "version": "1.1.12",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
+      "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0",
+        "concat-map": "0.0.1"
+      }
+    },
+    "node_modules/eslint/node_modules/eslint-visitor-keys": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/eslint/node_modules/ignore": {
+      "version": "5.3.2",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
+      "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/eslint/node_modules/minimatch": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
+      "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^1.1.7"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/espree": {
+      "version": "10.4.0",
+      "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
+      "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "acorn": "^8.15.0",
+        "acorn-jsx": "^5.3.2",
+        "eslint-visitor-keys": "^4.2.1"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/espree/node_modules/eslint-visitor-keys": {
+      "version": "4.2.1",
+      "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+      "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/eslint"
+      }
+    },
+    "node_modules/esquery": {
+      "version": "1.6.0",
+      "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
+      "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "estraverse": "^5.1.0"
+      },
+      "engines": {
+        "node": ">=0.10"
+      }
+    },
+    "node_modules/esrecurse": {
+      "version": "4.3.0",
+      "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
+      "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "estraverse": "^5.2.0"
+      },
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estraverse": {
+      "version": "5.3.0",
+      "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
+      "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=4.0"
+      }
+    },
+    "node_modules/estree-walker": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
+      "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "^1.0.0"
+      }
+    },
+    "node_modules/esutils": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
+      "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/eventemitter3": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
+      "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==",
+      "license": "MIT"
+    },
+    "node_modules/expand-template": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
+      "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
+      "license": "(MIT OR WTFPL)",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/expect-type": {
+      "version": "1.2.2",
+      "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz",
+      "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "engines": {
+        "node": ">=12.0.0"
+      }
+    },
+    "node_modules/fast-copy": {
+      "version": "3.0.2",
+      "resolved": "https://registry.npmjs.org/fast-copy/-/fast-copy-3.0.2.tgz",
+      "integrity": "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==",
+      "license": "MIT"
+    },
+    "node_modules/fast-decode-uri-component": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz",
+      "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==",
+      "license": "MIT"
+    },
+    "node_modules/fast-deep-equal": {
+      "version": "3.1.3",
+      "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+      "license": "MIT"
+    },
+    "node_modules/fast-glob": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
+      "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@nodelib/fs.stat": "^2.0.2",
+        "@nodelib/fs.walk": "^1.2.3",
+        "glob-parent": "^5.1.2",
+        "merge2": "^1.3.0",
+        "micromatch": "^4.0.8"
+      },
+      "engines": {
+        "node": ">=8.6.0"
+      }
+    },
+    "node_modules/fast-glob/node_modules/glob-parent": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
+      "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/fast-json-stable-stringify": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+      "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-json-stringify": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.0.1.tgz",
+      "integrity": "sha512-s7SJE83QKBZwg54dIbD5rCtzOBVD43V1ReWXXYqBgwCwHLYAAT0RQc/FmrQglXqWPpz6omtryJQOau5jI4Nrvg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@fastify/merge-json-schemas": "^0.2.0",
+        "ajv": "^8.12.0",
+        "ajv-formats": "^3.0.1",
+        "fast-uri": "^3.0.0",
+        "json-schema-ref-resolver": "^2.0.0",
+        "rfdc": "^1.2.0"
+      }
+    },
+    "node_modules/fast-json-stringify/node_modules/ajv": {
+      "version": "8.17.1",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
+      "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-uri": "^3.0.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/fast-json-stringify/node_modules/json-schema-traverse": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
+      "license": "MIT"
+    },
+    "node_modules/fast-levenshtein": {
+      "version": "2.0.6",
+      "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+      "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/fast-querystring": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz",
+      "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-decode-uri-component": "^1.0.1"
+      }
+    },
+    "node_modules/fast-redact": {
+      "version": "3.5.0",
+      "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz",
+      "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/fast-safe-stringify": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz",
+      "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==",
+      "license": "MIT"
+    },
+    "node_modules/fast-uri": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz",
+      "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/fastify": {
+      "version": "5.6.0",
+      "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.6.0.tgz",
+      "integrity": "sha512-9j2r9TnwNsfGiCKGYT0Voqy244qwcoYM9qvNi/i+F8sNNWDnqUEVuGYNc9GyjldhXmMlJmVPS6gI1LdvjYGRJw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@fastify/ajv-compiler": "^4.0.0",
+        "@fastify/error": "^4.0.0",
+        "@fastify/fast-json-stringify-compiler": "^5.0.0",
+        "@fastify/proxy-addr": "^5.0.0",
+        "abstract-logging": "^2.0.1",
+        "avvio": "^9.0.0",
+        "fast-json-stringify": "^6.0.0",
+        "find-my-way": "^9.0.0",
+        "light-my-request": "^6.0.0",
+        "pino": "^9.0.0",
+        "process-warning": "^5.0.0",
+        "rfdc": "^1.3.1",
+        "secure-json-parse": "^4.0.0",
+        "semver": "^7.6.0",
+        "toad-cache": "^3.7.0"
+      }
+    },
+    "node_modules/fastify-plugin": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/fastify-plugin/-/fastify-plugin-5.0.1.tgz",
+      "integrity": "sha512-HCxs+YnRaWzCl+cWRYFnHmeRFyR5GVnJTAaCJQiYzQSDwK9MgJdyAsuL3nh0EWRCYMgQ5MeziymvmAhUHYHDUQ==",
+      "license": "MIT"
+    },
+    "node_modules/fastq": {
+      "version": "1.19.1",
+      "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz",
+      "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==",
+      "license": "ISC",
+      "dependencies": {
+        "reusify": "^1.0.4"
+      }
+    },
+    "node_modules/file-entry-cache": {
+      "version": "8.0.0",
+      "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+      "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flat-cache": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=16.0.0"
+      }
+    },
+    "node_modules/file-uri-to-path": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
+      "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==",
+      "license": "MIT"
+    },
+    "node_modules/fill-range": {
+      "version": "7.1.1",
+      "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
+      "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "to-regex-range": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/find-my-way": {
+      "version": "9.3.0",
+      "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.3.0.tgz",
+      "integrity": "sha512-eRoFWQw+Yv2tuYlK2pjFS2jGXSxSppAs3hSQjfxVKxM5amECzIgYYc1FEI8ZmhSh/Ig+FrKEz43NLRKJjYCZVg==",
+      "license": "MIT",
+      "dependencies": {
+        "fast-deep-equal": "^3.1.3",
+        "fast-querystring": "^1.0.0",
+        "safe-regex2": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=20"
+      }
+    },
+    "node_modules/find-up": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+      "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "locate-path": "^6.0.0",
+        "path-exists": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/flat-cache": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
+      "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "flatted": "^3.2.9",
+        "keyv": "^4.5.4"
+      },
+      "engines": {
+        "node": ">=16"
+      }
+    },
+    "node_modules/flatted": {
+      "version": "3.3.3",
+      "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
+      "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/fs-constants": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
+      "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
+      "license": "MIT"
+    },
+    "node_modules/fsevents": {
+      "version": "2.3.3",
+      "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+      "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+      "dev": true,
+      "hasInstallScript": true,
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "darwin"
+      ],
+      "engines": {
+        "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+      }
+    },
+    "node_modules/get-tsconfig": {
+      "version": "4.10.1",
+      "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
+      "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "resolve-pkg-maps": "^1.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+      }
+    },
+    "node_modules/github-from-package": {
+      "version": "0.0.0",
+      "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
+      "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
+      "license": "MIT"
+    },
+    "node_modules/glob-parent": {
+      "version": "6.0.2",
+      "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
+      "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "is-glob": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=10.13.0"
+      }
+    },
+    "node_modules/globals": {
+      "version": "14.0.0",
+      "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
+      "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/graphemer": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz",
+      "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/has-flag": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+      "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/help-me": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/help-me/-/help-me-5.0.0.tgz",
+      "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==",
+      "license": "MIT"
+    },
+    "node_modules/ieee754": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+      "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/ignore": {
+      "version": "7.0.5",
+      "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+      "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 4"
+      }
+    },
+    "node_modules/import-fresh": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
+      "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "parent-module": "^1.0.0",
+        "resolve-from": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/imurmurhash": {
+      "version": "0.1.4",
+      "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
+      "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.8.19"
+      }
+    },
+    "node_modules/inherits": {
+      "version": "2.0.4",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+      "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+      "license": "ISC"
+    },
+    "node_modules/ini": {
+      "version": "1.3.8",
+      "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+      "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+      "license": "ISC"
+    },
+    "node_modules/ipaddr.js": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz",
+      "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/is-extglob": {
+      "version": "2.1.1",
+      "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+      "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-glob": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+      "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-extglob": "^2.1.1"
+      },
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/is-number": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
+      "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.12.0"
+      }
+    },
+    "node_modules/isexe": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+      "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/isows": {
+      "version": "1.0.7",
+      "resolved": "https://registry.npmjs.org/isows/-/isows-1.0.7.tgz",
+      "integrity": "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "peerDependencies": {
+        "ws": "*"
+      }
+    },
+    "node_modules/joycon": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz",
+      "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/js-tokens": {
+      "version": "9.0.1",
+      "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz",
+      "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/js-yaml": {
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
+      "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "argparse": "^2.0.1"
+      },
+      "bin": {
+        "js-yaml": "bin/js-yaml.js"
+      }
+    },
+    "node_modules/json-buffer": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
+      "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-schema-ref-resolver": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-2.0.1.tgz",
+      "integrity": "sha512-HG0SIB9X4J8bwbxCbnd5FfPEbcXAJYTi1pBJeP/QPON+w8ovSME8iRG+ElHNxZNX2Qh6eYn1GdzJFS4cDFfx0Q==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "dequal": "^2.0.3"
+      }
+    },
+    "node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/json-stable-stringify-without-jsonify": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+      "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/keyv": {
+      "version": "4.5.4",
+      "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
+      "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "json-buffer": "3.0.1"
+      }
+    },
+    "node_modules/levn": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
+      "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1",
+        "type-check": "~0.4.0"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/light-my-request": {
+      "version": "6.6.0",
+      "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz",
+      "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause",
+      "dependencies": {
+        "cookie": "^1.0.1",
+        "process-warning": "^4.0.0",
+        "set-cookie-parser": "^2.6.0"
+      }
+    },
+    "node_modules/light-my-request/node_modules/process-warning": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz",
+      "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/locate-path": {
+      "version": "6.0.0",
+      "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+      "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-locate": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/lodash.merge": {
+      "version": "4.6.2",
+      "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
+      "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/loupe": {
+      "version": "3.2.1",
+      "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
+      "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/magic-string": {
+      "version": "0.30.19",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.19.tgz",
+      "integrity": "sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@jridgewell/sourcemap-codec": "^1.5.5"
+      }
+    },
+    "node_modules/merge2": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
+      "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/micromatch": {
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "braces": "^3.0.3",
+        "picomatch": "^2.3.1"
+      },
+      "engines": {
+        "node": ">=8.6"
+      }
+    },
+    "node_modules/mimic-response": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
+      "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/minimist": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
+      "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/mkdirp-classic": {
+      "version": "0.5.3",
+      "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
+      "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
+      "license": "MIT"
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/nanoid": {
+      "version": "3.3.11",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+      "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "bin": {
+        "nanoid": "bin/nanoid.cjs"
+      },
+      "engines": {
+        "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+      }
+    },
+    "node_modules/napi-build-utils": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
+      "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
+      "license": "MIT"
+    },
+    "node_modules/natural-compare": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
+      "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/node-abi": {
+      "version": "3.77.0",
+      "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.77.0.tgz",
+      "integrity": "sha512-DSmt0OEcLoK4i3NuscSbGjOf3bqiDEutejqENSplMSFA/gmB8mkED9G4pKWnPl7MDU4rSHebKPHeitpDfyH0cQ==",
+      "license": "MIT",
+      "dependencies": {
+        "semver": "^7.3.5"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/node-addon-api": {
+      "version": "8.5.0",
+      "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
+      "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
+      "license": "MIT",
+      "engines": {
+        "node": "^18 || ^20 || >= 21"
+      }
+    },
+    "node_modules/on-exit-leak-free": {
+      "version": "2.1.2",
+      "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz",
+      "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/once": {
+      "version": "1.4.0",
+      "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
+      "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
+      "license": "ISC",
+      "dependencies": {
+        "wrappy": "1"
+      }
+    },
+    "node_modules/optionator": {
+      "version": "0.9.4",
+      "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
+      "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "deep-is": "^0.1.3",
+        "fast-levenshtein": "^2.0.6",
+        "levn": "^0.4.1",
+        "prelude-ls": "^1.2.1",
+        "type-check": "^0.4.0",
+        "word-wrap": "^1.2.5"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/ox": {
+      "version": "0.9.3",
+      "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz",
+      "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@adraffy/ens-normalize": "^1.11.0",
+        "@noble/ciphers": "^1.3.0",
+        "@noble/curves": "1.9.1",
+        "@noble/hashes": "^1.8.0",
+        "@scure/bip32": "^1.7.0",
+        "@scure/bip39": "^1.6.0",
+        "abitype": "^1.0.9",
+        "eventemitter3": "5.0.1"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.4.0"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/p-limit": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+      "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "yocto-queue": "^0.1.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-locate": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+      "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "p-limit": "^3.0.2"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-queue": {
+      "version": "8.1.1",
+      "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-8.1.1.tgz",
+      "integrity": "sha512-aNZ+VfjobsWryoiPnEApGGmf5WmNsCo9xu8dfaYamG5qaLP7ClhLN6NgsFe6SwJ2UbLEBK5dv9x8Mn5+RVhMWQ==",
+      "license": "MIT",
+      "dependencies": {
+        "eventemitter3": "^5.0.1",
+        "p-timeout": "^6.1.2"
+      },
+      "engines": {
+        "node": ">=18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/p-timeout": {
+      "version": "6.1.4",
+      "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz",
+      "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/parent-module": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
+      "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "callsites": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/path-exists": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+      "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/path-key": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+      "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/pathe": {
+      "version": "2.0.3",
+      "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+      "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/pathval": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz",
+      "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14.16"
+      }
+    },
+    "node_modules/picocolors": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/picomatch": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
+      "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8.6"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/pino": {
+      "version": "9.9.5",
+      "resolved": "https://registry.npmjs.org/pino/-/pino-9.9.5.tgz",
+      "integrity": "sha512-d1s98p8/4TfYhsJ09r/Azt30aYELRi6NNnZtEbqFw6BoGsdPVf5lKNK3kUwH8BmJJfpTLNuicjUQjaMbd93dVg==",
+      "license": "MIT",
+      "dependencies": {
+        "atomic-sleep": "^1.0.0",
+        "fast-redact": "^3.1.1",
+        "on-exit-leak-free": "^2.1.0",
+        "pino-abstract-transport": "^2.0.0",
+        "pino-std-serializers": "^7.0.0",
+        "process-warning": "^5.0.0",
+        "quick-format-unescaped": "^4.0.3",
+        "real-require": "^0.2.0",
+        "safe-stable-stringify": "^2.3.1",
+        "sonic-boom": "^4.0.1",
+        "thread-stream": "^3.0.0"
+      },
+      "bin": {
+        "pino": "bin.js"
+      }
+    },
+    "node_modules/pino-abstract-transport": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz",
+      "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==",
+      "license": "MIT",
+      "dependencies": {
+        "split2": "^4.0.0"
+      }
+    },
+    "node_modules/pino-pretty": {
+      "version": "13.1.1",
+      "resolved": "https://registry.npmjs.org/pino-pretty/-/pino-pretty-13.1.1.tgz",
+      "integrity": "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA==",
+      "license": "MIT",
+      "dependencies": {
+        "colorette": "^2.0.7",
+        "dateformat": "^4.6.3",
+        "fast-copy": "^3.0.2",
+        "fast-safe-stringify": "^2.1.1",
+        "help-me": "^5.0.0",
+        "joycon": "^3.1.1",
+        "minimist": "^1.2.6",
+        "on-exit-leak-free": "^2.1.0",
+        "pino-abstract-transport": "^2.0.0",
+        "pump": "^3.0.0",
+        "secure-json-parse": "^4.0.0",
+        "sonic-boom": "^4.0.1",
+        "strip-json-comments": "^5.0.2"
+      },
+      "bin": {
+        "pino-pretty": "bin.js"
+      }
+    },
+    "node_modules/pino-pretty/node_modules/strip-json-comments": {
+      "version": "5.0.3",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-5.0.3.tgz",
+      "integrity": "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.16"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/pino-std-serializers": {
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.0.0.tgz",
+      "integrity": "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA==",
+      "license": "MIT"
+    },
+    "node_modules/postcss": {
+      "version": "8.5.6",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+      "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/postcss/"
+        },
+        {
+          "type": "tidelift",
+          "url": "https://tidelift.com/funding/github/npm/postcss"
+        },
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/ai"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "nanoid": "^3.3.11",
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
+      },
+      "engines": {
+        "node": "^10 || ^12 || >=14"
+      }
+    },
+    "node_modules/prebuild-install": {
+      "version": "7.1.3",
+      "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
+      "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
+      "license": "MIT",
+      "dependencies": {
+        "detect-libc": "^2.0.0",
+        "expand-template": "^2.0.3",
+        "github-from-package": "0.0.0",
+        "minimist": "^1.2.3",
+        "mkdirp-classic": "^0.5.3",
+        "napi-build-utils": "^2.0.0",
+        "node-abi": "^3.3.0",
+        "pump": "^3.0.0",
+        "rc": "^1.2.7",
+        "simple-get": "^4.0.0",
+        "tar-fs": "^2.0.0",
+        "tunnel-agent": "^0.6.0"
+      },
+      "bin": {
+        "prebuild-install": "bin.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/prelude-ls": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
+      "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/process-warning": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
+      "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/pump": {
+      "version": "3.0.3",
+      "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+      "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==",
+      "license": "MIT",
+      "dependencies": {
+        "end-of-stream": "^1.1.0",
+        "once": "^1.3.1"
+      }
+    },
+    "node_modules/punycode": {
+      "version": "2.3.1",
+      "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
+      "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/queue-microtask": {
+      "version": "1.2.3",
+      "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
+      "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/quick-format-unescaped": {
+      "version": "4.0.4",
+      "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
+      "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==",
+      "license": "MIT"
+    },
+    "node_modules/rc": {
+      "version": "1.2.8",
+      "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
+      "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
+      "license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
+      "dependencies": {
+        "deep-extend": "^0.6.0",
+        "ini": "~1.3.0",
+        "minimist": "^1.2.0",
+        "strip-json-comments": "~2.0.1"
+      },
+      "bin": {
+        "rc": "cli.js"
+      }
+    },
+    "node_modules/rc/node_modules/strip-json-comments": {
+      "version": "2.0.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
+      "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/real-require": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz",
+      "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 12.13.0"
+      }
+    },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/resolve-from": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
+      "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=4"
+      }
+    },
+    "node_modules/resolve-pkg-maps": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+      "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+      "dev": true,
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+      }
+    },
+    "node_modules/ret": {
+      "version": "0.5.0",
+      "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz",
+      "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/reusify": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz",
+      "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==",
+      "license": "MIT",
+      "engines": {
+        "iojs": ">=1.0.0",
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/rfdc": {
+      "version": "1.4.1",
+      "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz",
+      "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
+      "license": "MIT"
+    },
+    "node_modules/rollup": {
+      "version": "4.50.1",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.50.1.tgz",
+      "integrity": "sha512-78E9voJHwnXQMiQdiqswVLZwJIzdBKJ1GdI5Zx6XwoFKUIk09/sSrr+05QFzvYb8q6Y9pPV45zzDuYa3907TZA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/estree": "1.0.8"
+      },
+      "bin": {
+        "rollup": "dist/bin/rollup"
+      },
+      "engines": {
+        "node": ">=18.0.0",
+        "npm": ">=8.0.0"
+      },
+      "optionalDependencies": {
+        "@rollup/rollup-android-arm-eabi": "4.50.1",
+        "@rollup/rollup-android-arm64": "4.50.1",
+        "@rollup/rollup-darwin-arm64": "4.50.1",
+        "@rollup/rollup-darwin-x64": "4.50.1",
+        "@rollup/rollup-freebsd-arm64": "4.50.1",
+        "@rollup/rollup-freebsd-x64": "4.50.1",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.50.1",
+        "@rollup/rollup-linux-arm-musleabihf": "4.50.1",
+        "@rollup/rollup-linux-arm64-gnu": "4.50.1",
+        "@rollup/rollup-linux-arm64-musl": "4.50.1",
+        "@rollup/rollup-linux-loongarch64-gnu": "4.50.1",
+        "@rollup/rollup-linux-ppc64-gnu": "4.50.1",
+        "@rollup/rollup-linux-riscv64-gnu": "4.50.1",
+        "@rollup/rollup-linux-riscv64-musl": "4.50.1",
+        "@rollup/rollup-linux-s390x-gnu": "4.50.1",
+        "@rollup/rollup-linux-x64-gnu": "4.50.1",
+        "@rollup/rollup-linux-x64-musl": "4.50.1",
+        "@rollup/rollup-openharmony-arm64": "4.50.1",
+        "@rollup/rollup-win32-arm64-msvc": "4.50.1",
+        "@rollup/rollup-win32-ia32-msvc": "4.50.1",
+        "@rollup/rollup-win32-x64-msvc": "4.50.1",
+        "fsevents": "~2.3.2"
+      }
+    },
+    "node_modules/run-parallel": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
+      "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==",
+      "dev": true,
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "queue-microtask": "^1.2.2"
+      }
+    },
+    "node_modules/safe-buffer": {
+      "version": "5.2.1",
+      "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+      "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/safe-regex2": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.0.0.tgz",
+      "integrity": "sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "ret": "~0.5.0"
+      }
+    },
+    "node_modules/safe-stable-stringify": {
+      "version": "2.5.0",
+      "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz",
+      "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/secure-json-parse": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.0.0.tgz",
+      "integrity": "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/fastify"
+        },
+        {
+          "type": "opencollective",
+          "url": "https://opencollective.com/fastify"
+        }
+      ],
+      "license": "BSD-3-Clause"
+    },
+    "node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
+    "node_modules/set-cookie-parser": {
+      "version": "2.7.1",
+      "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
+      "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==",
+      "license": "MIT"
+    },
+    "node_modules/shebang-command": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+      "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "shebang-regex": "^3.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/shebang-regex": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+      "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/siginfo": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+      "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+      "dev": true,
+      "license": "ISC"
+    },
+    "node_modules/simple-concat": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
+      "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT"
+    },
+    "node_modules/simple-get": {
+      "version": "4.0.1",
+      "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
+      "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/feross"
+        },
+        {
+          "type": "patreon",
+          "url": "https://www.patreon.com/feross"
+        },
+        {
+          "type": "consulting",
+          "url": "https://feross.org/support"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "decompress-response": "^6.0.0",
+        "once": "^1.3.1",
+        "simple-concat": "^1.0.0"
+      }
+    },
+    "node_modules/sonic-boom": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.0.tgz",
+      "integrity": "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==",
+      "license": "MIT",
+      "dependencies": {
+        "atomic-sleep": "^1.0.0"
+      }
+    },
+    "node_modules/source-map-js": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+      "dev": true,
+      "license": "BSD-3-Clause",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/split2": {
+      "version": "4.2.0",
+      "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+      "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+      "license": "ISC",
+      "engines": {
+        "node": ">= 10.x"
+      }
+    },
+    "node_modules/stackback": {
+      "version": "0.0.2",
+      "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+      "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/std-env": {
+      "version": "3.9.0",
+      "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz",
+      "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/string_decoder": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+      "license": "MIT",
+      "dependencies": {
+        "safe-buffer": "~5.2.0"
+      }
+    },
+    "node_modules/strip-json-comments": {
+      "version": "3.1.1",
+      "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+      "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=8"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/strip-literal": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz",
+      "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "js-tokens": "^9.0.1"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      }
+    },
+    "node_modules/supports-color": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+      "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "has-flag": "^4.0.0"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/tar-fs": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
+      "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
+      "license": "MIT",
+      "dependencies": {
+        "chownr": "^1.1.1",
+        "mkdirp-classic": "^0.5.2",
+        "pump": "^3.0.0",
+        "tar-stream": "^2.1.4"
+      }
+    },
+    "node_modules/tar-stream": {
+      "version": "2.2.0",
+      "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
+      "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
+      "license": "MIT",
+      "dependencies": {
+        "bl": "^4.0.3",
+        "end-of-stream": "^1.4.1",
+        "fs-constants": "^1.0.0",
+        "inherits": "^2.0.3",
+        "readable-stream": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/tar-stream/node_modules/readable-stream": {
+      "version": "3.6.2",
+      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+      "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+      "license": "MIT",
+      "dependencies": {
+        "inherits": "^2.0.3",
+        "string_decoder": "^1.1.1",
+        "util-deprecate": "^1.0.1"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/thread-stream": {
+      "version": "3.1.0",
+      "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz",
+      "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==",
+      "license": "MIT",
+      "dependencies": {
+        "real-require": "^0.2.0"
+      }
+    },
+    "node_modules/tinybench": {
+      "version": "2.9.0",
+      "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+      "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyexec": {
+      "version": "0.3.2",
+      "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz",
+      "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/tinyglobby": {
+      "version": "0.2.15",
+      "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+      "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3"
+      },
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/SuperchupuDev"
+      }
+    },
+    "node_modules/tinyglobby/node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tinyglobby/node_modules/picomatch": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/tinypool": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz",
+      "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": "^18.0.0 || >=20.0.0"
+      }
+    },
+    "node_modules/tinyrainbow": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz",
+      "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/tinyspy": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz",
+      "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=14.0.0"
+      }
+    },
+    "node_modules/to-regex-range": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
+      "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "is-number": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=8.0"
+      }
+    },
+    "node_modules/toad-cache": {
+      "version": "3.7.0",
+      "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz",
+      "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/ts-api-utils": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
+      "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18.12"
+      },
+      "peerDependencies": {
+        "typescript": ">=4.8.4"
+      }
+    },
+    "node_modules/tsx": {
+      "version": "4.20.5",
+      "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz",
+      "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "~0.25.0",
+        "get-tsconfig": "^4.7.5"
+      },
+      "bin": {
+        "tsx": "dist/cli.mjs"
+      },
+      "engines": {
+        "node": ">=18.0.0"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      }
+    },
+    "node_modules/tunnel-agent": {
+      "version": "0.6.0",
+      "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
+      "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
+      "license": "Apache-2.0",
+      "dependencies": {
+        "safe-buffer": "^5.0.1"
+      },
+      "engines": {
+        "node": "*"
+      }
+    },
+    "node_modules/type-check": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
+      "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "prelude-ls": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 0.8.0"
+      }
+    },
+    "node_modules/typescript": {
+      "version": "5.9.2",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz",
+      "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
+      "devOptional": true,
+      "license": "Apache-2.0",
+      "bin": {
+        "tsc": "bin/tsc",
+        "tsserver": "bin/tsserver"
+      },
+      "engines": {
+        "node": ">=14.17"
+      }
+    },
+    "node_modules/undici-types": {
+      "version": "6.21.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+      "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/uri-js": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
+      "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+      "dev": true,
+      "license": "BSD-2-Clause",
+      "dependencies": {
+        "punycode": "^2.1.0"
+      }
+    },
+    "node_modules/util-deprecate": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+      "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+      "license": "MIT"
+    },
+    "node_modules/viem": {
+      "version": "2.37.5",
+      "resolved": "https://registry.npmjs.org/viem/-/viem-2.37.5.tgz",
+      "integrity": "sha512-bLKvKgLcge6KWBMLk8iP9weu5tHNr0hkxPNwQd+YQrHEgek7ogTBBeE10T0V6blwBMYmeZFZHLnMhDmPjp63/A==",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/wevm"
+        }
+      ],
+      "license": "MIT",
+      "dependencies": {
+        "@noble/curves": "1.9.1",
+        "@noble/hashes": "1.8.0",
+        "@scure/bip32": "1.7.0",
+        "@scure/bip39": "1.6.0",
+        "abitype": "1.1.0",
+        "isows": "1.0.7",
+        "ox": "0.9.3",
+        "ws": "8.18.3"
+      },
+      "peerDependencies": {
+        "typescript": ">=5.0.4"
+      },
+      "peerDependenciesMeta": {
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite": {
+      "version": "7.1.5",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.5.tgz",
+      "integrity": "sha512-4cKBO9wR75r0BeIWWWId9XK9Lj6La5X846Zw9dFfzMRw38IlTk2iCcUt6hsyiDRcPidc55ZParFYDXi0nXOeLQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "esbuild": "^0.25.0",
+        "fdir": "^6.5.0",
+        "picomatch": "^4.0.3",
+        "postcss": "^8.5.6",
+        "rollup": "^4.43.0",
+        "tinyglobby": "^0.2.15"
+      },
+      "bin": {
+        "vite": "bin/vite.js"
+      },
+      "engines": {
+        "node": "^20.19.0 || >=22.12.0"
+      },
+      "funding": {
+        "url": "https://github.com/vitejs/vite?sponsor=1"
+      },
+      "optionalDependencies": {
+        "fsevents": "~2.3.3"
+      },
+      "peerDependencies": {
+        "@types/node": "^20.19.0 || >=22.12.0",
+        "jiti": ">=1.21.0",
+        "less": "^4.0.0",
+        "lightningcss": "^1.21.0",
+        "sass": "^1.70.0",
+        "sass-embedded": "^1.70.0",
+        "stylus": ">=0.54.8",
+        "sugarss": "^5.0.0",
+        "terser": "^5.16.0",
+        "tsx": "^4.8.1",
+        "yaml": "^2.4.2"
+      },
+      "peerDependenciesMeta": {
+        "@types/node": {
+          "optional": true
+        },
+        "jiti": {
+          "optional": true
+        },
+        "less": {
+          "optional": true
+        },
+        "lightningcss": {
+          "optional": true
+        },
+        "sass": {
+          "optional": true
+        },
+        "sass-embedded": {
+          "optional": true
+        },
+        "stylus": {
+          "optional": true
+        },
+        "sugarss": {
+          "optional": true
+        },
+        "terser": {
+          "optional": true
+        },
+        "tsx": {
+          "optional": true
+        },
+        "yaml": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite-node": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz",
+      "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "cac": "^6.7.14",
+        "debug": "^4.4.1",
+        "es-module-lexer": "^1.7.0",
+        "pathe": "^2.0.3",
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0"
+      },
+      "bin": {
+        "vite-node": "vite-node.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      }
+    },
+    "node_modules/vite/node_modules/fdir": {
+      "version": "6.5.0",
+      "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+      "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12.0.0"
+      },
+      "peerDependencies": {
+        "picomatch": "^3 || ^4"
+      },
+      "peerDependenciesMeta": {
+        "picomatch": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vite/node_modules/picomatch": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/vitest": {
+      "version": "3.2.4",
+      "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz",
+      "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "@types/chai": "^5.2.2",
+        "@vitest/expect": "3.2.4",
+        "@vitest/mocker": "3.2.4",
+        "@vitest/pretty-format": "^3.2.4",
+        "@vitest/runner": "3.2.4",
+        "@vitest/snapshot": "3.2.4",
+        "@vitest/spy": "3.2.4",
+        "@vitest/utils": "3.2.4",
+        "chai": "^5.2.0",
+        "debug": "^4.4.1",
+        "expect-type": "^1.2.1",
+        "magic-string": "^0.30.17",
+        "pathe": "^2.0.3",
+        "picomatch": "^4.0.2",
+        "std-env": "^3.9.0",
+        "tinybench": "^2.9.0",
+        "tinyexec": "^0.3.2",
+        "tinyglobby": "^0.2.14",
+        "tinypool": "^1.1.1",
+        "tinyrainbow": "^2.0.0",
+        "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0",
+        "vite-node": "3.2.4",
+        "why-is-node-running": "^2.3.0"
+      },
+      "bin": {
+        "vitest": "vitest.mjs"
+      },
+      "engines": {
+        "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+      },
+      "funding": {
+        "url": "https://opencollective.com/vitest"
+      },
+      "peerDependencies": {
+        "@edge-runtime/vm": "*",
+        "@types/debug": "^4.1.12",
+        "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+        "@vitest/browser": "3.2.4",
+        "@vitest/ui": "3.2.4",
+        "happy-dom": "*",
+        "jsdom": "*"
+      },
+      "peerDependenciesMeta": {
+        "@edge-runtime/vm": {
+          "optional": true
+        },
+        "@types/debug": {
+          "optional": true
+        },
+        "@types/node": {
+          "optional": true
+        },
+        "@vitest/browser": {
+          "optional": true
+        },
+        "@vitest/ui": {
+          "optional": true
+        },
+        "happy-dom": {
+          "optional": true
+        },
+        "jsdom": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/vitest/node_modules/picomatch": {
+      "version": "4.0.3",
+      "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+      "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/jonschlinkert"
+      }
+    },
+    "node_modules/which": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+      "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "isexe": "^2.0.0"
+      },
+      "bin": {
+        "node-which": "bin/node-which"
+      },
+      "engines": {
+        "node": ">= 8"
+      }
+    },
+    "node_modules/why-is-node-running": {
+      "version": "2.3.0",
+      "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+      "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "siginfo": "^2.0.0",
+        "stackback": "0.0.2"
+      },
+      "bin": {
+        "why-is-node-running": "cli.js"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/word-wrap": {
+      "version": "1.2.5",
+      "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
+      "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
+    "node_modules/wrappy": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
+      "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
+      "license": "ISC"
+    },
+    "node_modules/ws": {
+      "version": "8.18.3",
+      "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
+      "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.0.0"
+      },
+      "peerDependencies": {
+        "bufferutil": "^4.0.1",
+        "utf-8-validate": ">=5.0.2"
+      },
+      "peerDependenciesMeta": {
+        "bufferutil": {
+          "optional": true
+        },
+        "utf-8-validate": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/yocto-queue": {
+      "version": "0.1.0",
+      "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+      "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    }
+  }
+}
diff --git a/sequencer/package.json b/sequencer/package.json
new file mode 100644
index 00000000..990f5215
--- /dev/null
+++ b/sequencer/package.json
@@ -0,0 +1,45 @@
+{
+  "name": "@facet/sequencer",
+  "version": "0.1.0",
+  "description": "Lightweight Facet transaction sequencer",
+  "main": "dist/index.js",
+  "type": "module",
+  "scripts": {
+    "dev": "tsx watch src/index.ts",
+    "build": "npm run typecheck && tsc",
+    "prebuild": "npm run lint",
+    "start": "node dist/index.js",
+    "test": "vitest",
+    "typecheck": "tsc --noEmit",
+    "lint": "eslint src --ext .ts --max-warnings 5",
+    "migrate": "tsx src/db/migrate.ts",
+    "test:send": "tsx scripts/test-transaction.ts",
+    "test:db": "tsx scripts/inspect-db.ts",
+    "test:e2e": "tsx scripts/test-e2e.ts",
+    "mint": "tsx scripts/mint-eth.ts"
+  },
+  "dependencies": {
+    "viem": "2.37.5",
+    "c-kzg": "4.1.0",
+    "better-sqlite3": "12.4.1",
+    "fastify": "5.6.0",
+    "@fastify/cors": "11.1.0",
+    "p-queue": "8.1.1",
+    "dotenv": "17.2.2",
+    "pino": "9.9.5",
+    "pino-pretty": "13.1.1"
+  },
+  "devDependencies": {
+    "@types/better-sqlite3": "7.6.13",
+    "@types/node": "^20.11.0",
+    "typescript": "5.9.2",
+    "tsx": "4.20.5",
+    "vitest": "3.2.4",
+    "@typescript-eslint/eslint-plugin": "8.43.0",
+    "@typescript-eslint/parser": "8.43.0",
+    "eslint": "9.35.0"
+  },
+  "engines": {
+    "node": ">=20.0.0"
+  }
+}
\ No newline at end of file
diff --git a/sequencer/scripts/inspect-db.ts b/sequencer/scripts/inspect-db.ts
new file mode 100644
index 00000000..7f8e7ae0
--- /dev/null
+++ b/sequencer/scripts/inspect-db.ts
@@ -0,0 +1,105 @@
+#!/usr/bin/env tsx
+
+import Database from 'better-sqlite3';
+import { resolve } from 'path';
+
+const DB_PATH = process.env.DB_PATH || './data/sequencer.db';
+
+function inspectDatabase() {
+  console.log('🔍 Inspecting Sequencer Database\n');
+  console.log('📁 Database path:', resolve(DB_PATH), '\n');
+  
+  const db = new Database(DB_PATH, { readonly: true });
+  
+  try {
+    // Get transaction count by state
+    console.log('📊 Transaction Statistics:');
+    const txStats = db.prepare(`
+      SELECT state, COUNT(*) as count 
+      FROM transactions 
+      GROUP BY state
+    `).all();
+    
+    for (const stat of txStats) {
+      console.log(`   ${stat.state}: ${stat.count}`);
+    }
+    
+    // Get recent transactions
+    console.log('\n📜 Recent Transactions (last 5):');
+    const recentTxs = db.prepare(`
+      SELECT 
+        '0x' || hex(hash) as hash,
+        '0x' || hex(from_address) as from_address,
+        nonce,
+        max_fee_per_gas,
+        state,
+        datetime(received_at/1000, 'unixepoch') as received_at
+      FROM transactions 
+      ORDER BY received_seq DESC 
+      LIMIT 5
+    `).all();
+    
+    for (const tx of recentTxs) {
+      console.log(`\n   Hash: ${tx.hash}`);
+      console.log(`   From: ${tx.from_address}`);
+      console.log(`   Nonce: ${tx.nonce}`);
+      console.log(`   Max Fee: ${tx.max_fee_per_gas} wei`);
+      console.log(`   State: ${tx.state}`);
+      console.log(`   Received: ${tx.received_at}`);
+    }
+    
+    // Get batch statistics
+    console.log('\n📦 Batch Statistics:');
+    const batchStats = db.prepare(`
+      SELECT state, COUNT(*) as count 
+      FROM batches 
+      GROUP BY state
+    `).all();
+    
+    if (batchStats.length === 0) {
+      console.log('   No batches created yet');
+    } else {
+      for (const stat of batchStats) {
+        console.log(`   ${stat.state}: ${stat.count}`);
+      }
+    }
+    
+    // Get recent batches
+    const recentBatches = db.prepare(`
+      SELECT 
+        id,
+        '0x' || hex(content_hash) as content_hash,
+        state,
+        tx_count,
+        blob_size,
+        datetime(sealed_at/1000, 'unixepoch') as sealed_at
+      FROM batches 
+      ORDER BY id DESC 
+      LIMIT 3
+    `).all();
+    
+    if (recentBatches.length > 0) {
+      console.log('\n📦 Recent Batches:');
+      for (const batch of recentBatches) {
+        console.log(`\n   Batch #${batch.id}`);
+        console.log(`   Content Hash: ${batch.content_hash}`);
+        console.log(`   State: ${batch.state}`);
+        console.log(`   Transactions: ${batch.tx_count}`);
+        console.log(`   Size: ${batch.blob_size} bytes`);
+        console.log(`   Sealed: ${batch.sealed_at || 'Not sealed'}`);
+      }
+    }
+    
+    // Show raw SQL query option
+    console.log('\n💡 Tip: You can also query directly with:');
+    console.log(`   sqlite3 ${DB_PATH} "SELECT * FROM transactions;"`);
+    
+  } catch (error: any) {
+    console.error('❌ Error reading database:', error.message);
+  } finally {
+    db.close();
+  }
+}
+
+// Run inspection
+inspectDatabase();
\ No newline at end of file
diff --git a/sequencer/scripts/mint-eth.ts b/sequencer/scripts/mint-eth.ts
new file mode 100644
index 00000000..9dda9cdb
--- /dev/null
+++ b/sequencer/scripts/mint-eth.ts
@@ -0,0 +1,220 @@
+#!/usr/bin/env tsx
+
+import {
+  createWalletClient,
+  createPublicClient,
+  http,
+  type Hex,
+  parseEther,
+  formatEther,
+  toHex,
+  toRlp,
+  concatHex,
+  pad,
+  numberToHex
+} from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+import { defineChain } from 'viem';
+import { holesky, sepolia } from 'viem/chains';
+
+// Configuration
+const L1_RPC = process.env.L1_RPC_URL || 'https://ethereum-hoodi-rpc.publicnode.com';
+const L1_CHAIN = process.env.L1_CHAIN || 'hoodi';
+const L2_RPC = 'http://localhost:9545'; // Direct to Facet geth
+const FACET_INBOX_ADDRESS = '0x00000000000000000000000000000000000face7' as Hex;
+const FACET_TX_TYPE = 0x46; // Facet transaction type
+const L2_CHAIN_ID = 0xface7b; // Facet chain ID
+
+// Private key to use for minting (needs L1 ETH for gas!)
+const MINTER_PRIVATE_KEY = process.env.PRIVATE_KEY!;
+
+// Helper to create a Facet transaction payload
+function createFacetDepositPayload(recipientAddress: Hex): Hex {
+  // Create a simple deposit Facet transaction
+  // Structure: [chain_id, to, value, max_gas_fee, gas_limit, data]
+  const facetTx = [
+    toHex(L2_CHAIN_ID),                    // chain_id (Facet L2)
+    recipientAddress,                       // to (recipient on L2)
+    '0x' as Hex,                              // value (0 for deposit)
+    toHex(21000),                     // max_gas_fee (1 gwei)
+    '0x' as Hex,                            // data (empty),
+    '0x' as Hex                            // data (empty)
+  ];
+
+  // RLP encode the Facet transaction
+  const rlpEncoded = toRlp(facetTx);
+
+  // Prepend the Facet transaction type (0x46)
+  const typePrefix = toHex(FACET_TX_TYPE, { size: 1 });
+  const facetPayload = concatHex([typePrefix, rlpEncoded]);
+
+  return facetPayload;
+}
+
+async function mintEth() {
+  console.log('💰 Minting ETH on Facet L2\n');
+
+  // Create account from private key
+  const minter = privateKeyToAccount(MINTER_PRIVATE_KEY as Hex);
+  console.log('🔑 Minter account:', minter.address);
+
+  // Determine L1 chain
+  let l1Chain;
+  if (L1_CHAIN === 'holesky') {
+    l1Chain = holesky;
+  } else if (L1_CHAIN === 'sepolia') {
+    l1Chain = sepolia;
+  } else {
+    // Assume Hoodi
+    l1Chain = defineChain({
+      id: 560048,
+      name: 'Hoodi',
+      nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+      rpcUrls: {
+        default: { http: [L1_RPC] }
+      }
+    });
+  }
+
+  // Create L1 wallet client
+  const l1Wallet = createWalletClient({
+    account: minter,
+    chain: l1Chain,
+    transport: http(L1_RPC)
+  });
+
+  // Create L1 public client for balance checks
+  const l1Client = createPublicClient({
+    chain: l1Chain,
+    transport: http(L1_RPC)
+  });
+
+  // Create L2 public client to check balances
+  const l2Client = createPublicClient({
+    chain: defineChain({
+      id: L2_CHAIN_ID,
+      name: 'Facet',
+      nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+      rpcUrls: {
+        default: { http: [L2_RPC] }
+      }
+    }),
+    transport: http(L2_RPC)
+  });
+
+  try {
+    // Check L1 balance
+    const l1Balance = await l1Client.getBalance({
+      address: minter.address
+    });
+    console.log('💵 L1 balance:', formatEther(l1Balance), 'ETH');
+
+    if (l1Balance === 0n) {
+      console.error('❌ No L1 ETH! You need L1 ETH to send the deposit transaction.');
+      console.error('   Please fund this account on L1:', minter.address);
+      process.exit(1);
+    }
+
+    // Check initial L2 balance
+    const initialL2Balance = await l2Client.getBalance({
+      address: minter.address
+    }).catch(() => 0n);
+
+    console.log('💵 Initial L2 balance:', formatEther(initialL2Balance), 'ETH');
+
+    // Get L1 nonce
+    const l1Nonce = await l1Client.getTransactionCount({
+      address: minter.address,
+      blockTag: 'latest'
+    });
+
+    console.log('📝 L1 nonce:', l1Nonce);
+
+    // Create the Facet deposit payload
+    const facetPayload = createFacetDepositPayload(minter.address);
+    console.log('\n🔧 Created Facet deposit payload:', facetPayload);
+
+    // Create L1 transaction to send to Facet inbox
+    const depositTx = {
+      account: minter,
+      chain: l1Chain,
+      to: FACET_INBOX_ADDRESS,
+      value: 0n, // No value needed
+      data: facetPayload, // Facet transaction as payload
+      gas: 100000n, // More gas for processing the payload
+      maxFeePerGas: parseEther('0.00000003'), // 30 gwei
+      maxPriorityFeePerGas: parseEther('0.000000003'), // 3 gwei
+      nonce: l1Nonce,
+    };
+
+    console.log('\n📤 Sending deposit transaction on L1...');
+    console.log('   Chain:', L1_CHAIN);
+    console.log('   To (inbox):', FACET_INBOX_ADDRESS);
+    console.log('   Recipient on L2:', minter.address);
+
+    // Send L1 transaction
+    const hash = await l1Wallet.sendTransaction(depositTx);
+    console.log('\n✅ L1 transaction sent!');
+    console.log('   Hash:', hash);
+
+    // Wait for L1 confirmation
+    console.log('\n⏳ Waiting for L1 confirmation...');
+    const receipt = await l1Client.waitForTransactionReceipt({
+      hash,
+      confirmations: 1
+    });
+
+    if (receipt.status === 'success') {
+      console.log('✅ L1 transaction confirmed in block:', receipt.blockNumber);
+    } else {
+      console.error('❌ L1 transaction failed!');
+      process.exit(1);
+    }
+
+    // Now wait for L2 balance update
+    console.log('\n⏳ Waiting for L2 mint (this may take 30-60 seconds)...');
+    console.log('   The Facet importer needs to process the L1 block and derive the L2 state');
+
+    let attempts = 0;
+    const maxAttempts = 120; // 2 minutes
+    let finalL2Balance = initialL2Balance;
+
+    while (attempts < maxAttempts) {
+      await new Promise(r => setTimeout(r, 1000));
+
+      // Check new L2 balance
+      finalL2Balance = await l2Client.getBalance({
+        address: minter.address
+      }).catch(() => initialL2Balance);
+
+      if (finalL2Balance > initialL2Balance) {
+        console.log('\n🎉 ETH minted successfully on L2!');
+        console.log('   Initial L2 balance:', formatEther(initialL2Balance), 'ETH');
+        console.log('   Final L2 balance:', formatEther(finalL2Balance), 'ETH');
+        console.log('   Minted:', formatEther(finalL2Balance - initialL2Balance), 'ETH');
+        break;
+      }
+
+      process.stdout.write('.');
+      attempts++;
+    }
+
+    if (finalL2Balance === initialL2Balance) {
+      console.log('\n⚠️  L2 balance unchanged after 2 minutes');
+      console.log('   The deposit may still be processing');
+      console.log('   Check:');
+      console.log('   1. Facet importer logs for processing of block', receipt.blockNumber);
+      console.log('   2. Geth logs for L2 block derivation');
+    }
+
+  } catch (error: any) {
+    console.error('\n❌ Error:', error.message);
+    if (error.details) {
+      console.error('   Details:', error.details);
+    }
+    process.exit(1);
+  }
+}
+
+// Run the mint script
+mintEth().catch(console.error);
\ No newline at end of file
diff --git a/sequencer/scripts/test-e2e.ts b/sequencer/scripts/test-e2e.ts
new file mode 100644
index 00000000..6e160b5f
--- /dev/null
+++ b/sequencer/scripts/test-e2e.ts
@@ -0,0 +1,245 @@
+#!/usr/bin/env tsx
+
+import { 
+  createWalletClient, 
+  createPublicClient,
+  http, 
+  type Hex,
+  parseEther,
+  formatEther,
+  type TransactionSerializableEIP1559,
+  parseGwei
+} from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+import { defineChain } from 'viem';
+
+// Configuration
+const SEQUENCER_RPC = 'http://localhost:8547';
+const L2_RPC = 'http://localhost:9545'; // Your local geth/Facet node
+
+// Use the Anvil/Hardhat test accounts (they have ETH in local test networks)
+const TEST_ACCOUNTS = [
+  process.env.PRIVATE_KEY!, // Account 0
+];
+
+async function testE2E() {
+  console.log('🚀 Testing Facet E2E Flow\n');
+  
+  // Create accounts
+  const sender = privateKeyToAccount(TEST_ACCOUNTS[0] as Hex);
+  const receiver = sender
+  
+  console.log('📝 Test accounts:');
+  console.log('   Sender:', sender.address);
+  console.log('   Receiver:', receiver.address);
+  
+  // Define Facet chain
+  const facetChain = defineChain({
+    id: 0xface7b,
+    name: 'Facet Local',
+    nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+    rpcUrls: {
+      default: { http: [L2_RPC] }
+    }
+  });
+  
+  // Create L2 client to check balances
+  const l2Client = createPublicClient({
+    chain: facetChain,
+    transport: http(L2_RPC)
+  });
+  
+  // Check initial balances on L2
+  console.log('\n💰 Checking L2 balances...');
+  const senderBalance = await l2Client.getBalance({ address: sender.address });
+  const receiverBalance = await l2Client.getBalance({ address: receiver.address });
+  
+  console.log('   Sender balance:', formatEther(senderBalance), 'ETH');
+  console.log('   Receiver balance:', formatEther(receiverBalance), 'ETH');
+  
+  if (senderBalance === 0n) {
+    console.log('\n⚠️  Sender has no ETH on L2!');
+    console.log('   In local dev, accounts usually have pre-funded ETH.');
+    console.log('   You may need to fund the account or use a different test account.');
+  }
+  
+  // Get current nonce from L2
+  const nonce = await l2Client.getTransactionCount({ 
+    address: sender.address,
+    blockTag: 'latest'
+  });
+  console.log('   Sender nonce on L2:', nonce);
+  
+  // Create wallet client for sequencer
+  const sequencerWallet = createWalletClient({
+    account: sender,
+    chain: facetChain,
+    transport: http(SEQUENCER_RPC)
+  });
+  
+  // Create a real value transfer transaction
+  const tx: TransactionSerializableEIP1559 = {
+    type: 'eip1559',
+    chainId: 0xface7b,
+    nonce: Number(nonce),
+    to: receiver.address,
+    value: 0n, // Send 0.1 ETH
+    data: '0x7711',
+    gas: 75000n,
+    maxFeePerGas: parseGwei('20'), // 20 gwei
+    maxPriorityFeePerGas: parseGwei('0.000001'), // 2 gwei
+  };
+  
+  try {
+    console.log('\n📤 Sending transaction to sequencer...');
+    console.log('   From:', sender.address);
+    console.log('   To:', receiver.address);
+    console.log('   Value:', formatEther(tx.value!), 'ETH');
+    console.log('   Nonce:', tx.nonce);
+    console.log('   Gas:', tx.gas?.toString());
+    
+    // Send transaction to sequencer
+    const hash = await sequencerWallet.sendRawTransaction({
+      serializedTransaction: await sequencerWallet.signTransaction(tx)
+    });
+    
+    console.log('\n✅ Transaction accepted by sequencer!');
+    console.log('   Hash:', hash);
+    
+    // Check transaction status in sequencer
+    console.log('\n🔍 Checking sequencer status...');
+    const statusResponse = await fetch(SEQUENCER_RPC, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        jsonrpc: '2.0',
+        method: 'sequencer_getTxStatus',
+        params: [hash],
+        id: 1
+      })
+    });
+    
+    const statusResult = await statusResponse.json();
+    console.log('   Status:', JSON.stringify(statusResult.result, null, 2));
+    
+    // Wait for batch creation (3 seconds)
+    console.log('\n⏳ Waiting for batch creation (3 seconds)...');
+    await new Promise(r => setTimeout(r, 3500));
+    
+    // Check status again
+    const status2Response = await fetch(SEQUENCER_RPC, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        jsonrpc: '2.0',
+        method: 'sequencer_getTxStatus',
+        params: [hash],
+        id: 2
+      })
+    });
+    
+    const status2Result = await status2Response.json();
+    console.log('   Updated status:', JSON.stringify(status2Result.result, null, 2));
+    
+    // Monitor L2 for inclusion
+    console.log('\n👀 Monitoring L2 for transaction inclusion...');
+    console.log('   (This may take 30-60 seconds after L1 confirmation)');
+    
+    let included = false;
+    let attempts = 0;
+    const maxAttempts = 60; // Check for up to 1 minute
+    
+    // Heuristic: Check if OTHER transactions from the same batch made it
+    // This tells us if the derivation node processed the batch
+    let batchProcessed = false;
+    
+    while (!included && attempts < maxAttempts) {
+      try {
+        // First check if transaction exists in the node (even if failed)
+        const tx = await l2Client.getTransaction({ hash }).catch(() => null);
+        
+        if (tx) {
+          console.log('\n📦 Transaction found in L2 mempool/chain!');
+          console.log('   Transaction was derived from L1 batch');
+          
+          // Now check for receipt (execution result)
+          const receipt = await l2Client.getTransactionReceipt({ hash }).catch(() => null);
+          
+          if (receipt) {
+            console.log('\n🎯 Transaction executed on L2!');
+            console.log('   Block:', receipt.blockNumber);
+            console.log('   Gas Used:', receipt.gasUsed);
+            
+            if (receipt.status === 'success') {
+              console.log('   Status: ✅ Success');
+              
+              // Check final balances
+              const finalSenderBalance = await l2Client.getBalance({ address: sender.address });
+              const finalReceiverBalance = await l2Client.getBalance({ address: receiver.address });
+              
+              console.log('\n💰 Final L2 balances:');
+              console.log('   Sender:', formatEther(finalSenderBalance), 'ETH');
+              console.log('   Receiver:', formatEther(finalReceiverBalance), 'ETH');
+              console.log('   Receiver gained:', formatEther(finalReceiverBalance - receiverBalance), 'ETH');
+            } else {
+              console.log('   Status: ❌ Failed (reverted)');
+              console.log('\n⚠️  Transaction was included but failed execution');
+              console.log('   Possible reasons:');
+              console.log('   - Insufficient balance for transfer');
+              console.log('   - Contract revert');
+              console.log('   - Out of gas');
+              
+              // Still check balances to see the state
+              const finalSenderBalance = await l2Client.getBalance({ address: sender.address });
+              console.log('\n💰 Current L2 balance:');
+              console.log('   Sender:', formatEther(finalSenderBalance), 'ETH');
+              console.log('   (Transaction failed, no value transferred)');
+            }
+            
+            included = true;
+          } else {
+            // Transaction exists but no receipt yet - might still be pending
+            console.log('   Waiting for execution...');
+          }
+        }
+      } catch (e: any) {
+        // Check if this is a specific error about transaction not found
+        if (e.message?.includes('not found')) {
+          // Transaction genuinely not in L2 yet
+        } else {
+          // Some other error
+          console.log('   Error checking transaction:', e.message);
+        }
+      }
+      
+      if (!included) {
+        process.stdout.write('.');
+        await new Promise(r => setTimeout(r, 1000));
+        attempts++;
+      }
+    }
+    
+    if (!included) {
+      console.log('\n⚠️  Transaction not found in L2 after 1 minute');
+      console.log('\n   Debugging steps:');
+      console.log('   1. Check if batch was posted to L1:');
+      console.log(`      Check sequencer status for tx ${hash}`);
+      console.log('   2. Check derivation node logs for:');
+      console.log('      - "Found Facet batch" messages');
+      console.log('      - "Processing transaction" messages');
+      console.log('      - Any error messages');
+      console.log('   3. Check L2 node (geth) logs for:');
+      console.log('      - Transaction processing errors');
+      console.log('      - State changes');
+    }
+    
+  } catch (error: any) {
+    console.error('\n❌ Error:', error.message);
+    if (error.details) {
+      console.error('   Details:', error.details);
+    }
+  }
+}
+
+// Run the test
+testE2E().catch(console.error);
\ No newline at end of file
diff --git a/sequencer/scripts/test-transaction.ts b/sequencer/scripts/test-transaction.ts
new file mode 100644
index 00000000..29e42be2
--- /dev/null
+++ b/sequencer/scripts/test-transaction.ts
@@ -0,0 +1,131 @@
+#!/usr/bin/env tsx
+
+import { 
+  createWalletClient, 
+  createPublicClient,
+  http, 
+  type Hex,
+  parseEther,
+  type TransactionSerializableEIP1559
+} from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+import { defineChain } from 'viem';
+
+// Test configuration
+const SEQUENCER_RPC = 'http://localhost:8547';
+const TEST_PRIVATE_KEY = '';
+
+async function testSequencer() {
+  console.log('🚀 Testing Facet Sequencer\n');
+  
+  // Create test account
+  const account = privateKeyToAccount(TEST_PRIVATE_KEY as Hex);
+  console.log('📝 Test account:', account.address);
+  
+  // Define custom chain for sequencer
+  const facetChain = defineChain({
+    id: 0xface7b, // Facet chain ID (as number)
+    name: 'Facet (via Sequencer)',
+    nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+    rpcUrls: {
+      default: { http: [SEQUENCER_RPC] }
+    }
+  });
+  
+  // Create wallet client
+  const wallet = createWalletClient({
+    account,
+    chain: facetChain,
+    transport: http(SEQUENCER_RPC)
+  });
+  
+  // For L2 transactions, we need to track nonce properly
+  // In a real scenario, this would come from the L2 node
+  // For testing, we'll use an incrementing counter stored in a file or environment
+  let nonce = 0;
+  try {
+    // Try to read last nonce from a file
+    const fs = await import('fs');
+    const noncePath = '.test-nonce';
+    if (fs.existsSync(noncePath)) {
+      nonce = parseInt(fs.readFileSync(noncePath, 'utf8')) + 1;
+    }
+    // Save the new nonce for next run
+    fs.writeFileSync(noncePath, nonce.toString());
+  } catch (e) {
+    // If file operations fail, use timestamp-based for uniqueness
+    console.log('Using timestamp-based nonce for testing');
+    nonce = Math.floor(Date.now() / 1000) % 100000;
+  }
+  
+  // Create a test transaction
+  const tx: TransactionSerializableEIP1559 = {
+    type: 'eip1559',
+    chainId: 0xface7b,
+    nonce,
+    to: '0x70997970C51812dc3A010C7d01b50e0d17dc79C8' as Hex, // Random address
+    value: parseEther('0.001'),
+    gas: 21000n,
+    maxFeePerGas: parseEther('0.00000002'), // 20 gwei
+    maxPriorityFeePerGas: parseEther('0.000000002'), // 2 gwei
+  };
+  
+  try {
+    console.log('📤 Sending transaction to sequencer...');
+    console.log('   From:', account.address);
+    console.log('   To:', tx.to);
+    console.log('   Nonce:', nonce);
+    console.log('   Value:', tx.value?.toString(), 'wei');
+    console.log('   Gas:', tx.gas?.toString());
+    console.log('   Max Fee:', tx.maxFeePerGas?.toString(), 'wei\n');
+    
+    // Send transaction
+    const hash = await wallet.sendRawTransaction({
+      serializedTransaction: await wallet.signTransaction(tx)
+    });
+    
+    console.log('✅ Transaction accepted!');
+    console.log('   Hash:', hash, '\n');
+    
+    // Check transaction status
+    console.log('🔍 Checking transaction status...');
+    const statusResponse = await fetch(SEQUENCER_RPC, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        jsonrpc: '2.0',
+        method: 'sequencer_getTxStatus',
+        params: [hash],
+        id: 1
+      })
+    });
+    
+    const statusResult = await statusResponse.json();
+    console.log('   Status:', JSON.stringify(statusResult.result, null, 2), '\n');
+    
+    // Get sequencer stats
+    console.log('📊 Sequencer statistics:');
+    const statsResponse = await fetch(SEQUENCER_RPC, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        jsonrpc: '2.0',
+        method: 'sequencer_getStats',
+        params: [],
+        id: 2
+      })
+    });
+    
+    const statsResult = await statsResponse.json();
+    console.log(JSON.stringify(statsResult.result, null, 2));
+    
+  } catch (error: any) {
+    console.error('❌ Error:', error.message);
+    if (error.details) {
+      console.error('   Details:', error.details);
+    }
+  }
+}
+
+// Run the test
+testSequencer().catch(console.error);
\ No newline at end of file
diff --git a/sequencer/src/batch/maker.ts b/sequencer/src/batch/maker.ts
new file mode 100644
index 00000000..38a44bae
--- /dev/null
+++ b/sequencer/src/batch/maker.ts
@@ -0,0 +1,234 @@
+import { keccak256, toHex, concatHex, toRlp, size, type Hex, encodePacked } from 'viem';
+import type { DatabaseService } from '../db/schema.js';
+import { logger } from '../utils/logger.js';
+import type { PublicClient } from 'viem';
+
+interface Transaction {
+  hash: Buffer;
+  raw: Buffer;
+  from_address: Buffer;
+  nonce: number;
+  max_fee_per_gas: string;
+  intrinsic_gas: number;
+  received_seq: number;
+}
+
+export class BatchMaker {
+  private readonly MAX_PER_SENDER = 10;
+  private readonly MAX_BATCH_GAS = 30_000_000;
+  private readonly FACET_MAGIC_PREFIX = '0x756e73746f707061626c652073657175656e63696e67' as Hex; // "unstoppable sequencing"
+  private readonly L2_CHAIN_ID: bigint;
+  private readonly MAX_BLOB_SIZE = 131072; // 128KB
+  private lastBatchTime = Date.now();
+  
+  constructor(
+    private db: DatabaseService,
+    private l1Client: PublicClient,
+    l2ChainId: string
+  ) {
+    this.L2_CHAIN_ID = BigInt(l2ChainId);
+  }
+  
+  async createBatch(maxBytes: number = this.MAX_BLOB_SIZE - 1000, maxCount: number = 500): Promise {
+    const database = this.db.getDatabase();
+
+    // Get L1 data before starting the transaction
+    const gasBid = await this.calculateGasBid();
+
+    return database.transaction(() => {
+      // Select transactions ordered by fee
+      const candidates = database.prepare(`
+        SELECT * FROM transactions
+        WHERE state IN ('queued', 'requeued')
+        ORDER BY max_fee_per_gas DESC, received_seq ASC
+        LIMIT ?
+      `).all(maxCount * 2) as Transaction[];
+
+      if (candidates.length === 0) return null;
+
+      // Apply selection criteria
+      const selected = this.selectTransactions(candidates, maxBytes, maxCount);
+      if (selected.length === 0) return null;
+
+      const role = 0; // 0 = permissionless
+      const signature: Hex | undefined = undefined;
+
+      // Create Facet batch wire format
+      const wireFormat = this.createFacetWireFormat(selected, role, signature);
+      const contentHash = this.calculateContentHash(selected, role, signature);
+      
+      // Check for duplicate batch
+      const existing = database.prepare(
+        'SELECT id FROM batches WHERE content_hash = ?'
+      ).get(contentHash);
+      
+      if (existing) {
+        logger.warn({ contentHash: toHex(contentHash) }, 'Batch already exists');
+        return null;
+      }
+      
+      // Prepare the ordered transaction hashes for JSON storage
+      const txHashesJson = JSON.stringify(
+        selected.map(tx => '0x' + tx.hash.toString('hex'))
+      );
+
+      // Create batch record with tx_hashes as JSON
+      const batchResult = database.prepare(`
+        INSERT INTO batches (content_hash, wire_format, state, blob_size, gas_bid, tx_count, tx_hashes)
+        VALUES (?, ?, 'open', ?, ?, ?, ?)
+      `).run(
+        contentHash,
+        wireFormat,
+        wireFormat.length,
+        gasBid.toString(),
+        selected.length,
+        txHashesJson
+      );
+
+      const batchId = batchResult.lastInsertRowid as number;
+      
+      // Update transaction states
+      const updateTxs = database.prepare(`
+        UPDATE transactions 
+        SET state = 'batched', batch_id = ?
+        WHERE hash = ?
+      `);
+      
+      for (const tx of selected) {
+        updateTxs.run(batchId, tx.hash);
+      }
+      
+      // Seal the batch
+      database.prepare(
+        'UPDATE batches SET state = ?, sealed_at = ? WHERE id = ?'
+      ).run('sealed', Date.now(), batchId);
+      
+      logger.info({
+        batchId,
+        txCount: selected.length,
+        size: wireFormat.length
+      }, 'Batch created');
+      
+      return batchId;
+    })() as number | null;
+  }
+  
+  private selectTransactions(candidates: Transaction[], maxBytes: number, maxCount: number): Transaction[] {
+    const selected: Transaction[] = [];
+    const senderCounts = new Map();
+    let currentSize = 200; // Account for batch overhead
+    let currentGas = 0;
+    
+    for (const tx of candidates) {
+      // Size constraint
+      if (currentSize + tx.raw.length > maxBytes) continue;
+      
+      // Gas constraint  
+      if (currentGas + tx.intrinsic_gas > this.MAX_BATCH_GAS) continue;
+      
+      // Sender fairness
+      const senderKey = tx.from_address.toString('hex');
+      const count = senderCounts.get(senderKey) || 0;
+      if (count >= this.MAX_PER_SENDER) continue;
+      
+      selected.push(tx);
+      currentSize += tx.raw.length;
+      currentGas += tx.intrinsic_gas;
+      senderCounts.set(senderKey, count + 1);
+      
+      if (selected.length >= maxCount) break;
+    }
+    
+    return selected;
+  }
+  
+  private createFacetWireFormat(transactions: Transaction[], role: number, signature?: Hex): Buffer {
+    // RLP encode transaction list only (array of raw transaction bytes)
+    const txList = transactions.map(tx => ('0x' + tx.raw.toString('hex')) as Hex);
+    const rlpTxList = toRlp(txList);
+
+    // Build new wire format: [MAGIC:22][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST]
+    const rlpSize = size(rlpTxList);
+    const parts: Hex[] = [
+      this.FACET_MAGIC_PREFIX,                      // MAGIC: 12 bytes
+      encodePacked(['uint64'], [this.L2_CHAIN_ID]), // CHAIN_ID: 8 bytes big-endian
+      encodePacked(['uint8'], [1]),                 // VERSION: 1 byte
+      encodePacked(['uint8'], [role]),              // ROLE: 1 byte
+      toHex(rlpSize, { size: 4 }),                  // LENGTH: 4 bytes big-endian
+      rlpTxList                                     // RLP_TX_LIST
+    ];
+
+    if (signature) {
+      parts.push(signature);
+    }
+
+    const wireFormatHex = concatHex(parts);
+
+    return Buffer.from(wireFormatHex.slice(2), 'hex');
+  }
+
+  private calculateContentHash(transactions: Transaction[], role: number, signature?: Hex): Buffer {
+    // Calculate content hash from CHAIN_ID + VERSION + ROLE + RLP_TX_LIST + SIGNATURE
+    // Including signature ensures batches with different signatures don't deduplicate
+    // For permissionless batches (no signature), hash is just chain_id + version + role + txs
+    const txList = transactions.map(tx => ('0x' + tx.raw.toString('hex')) as Hex);
+    const rlpTxList = toRlp(txList);
+
+    const parts: Hex[] = [
+      encodePacked(['uint64'], [this.L2_CHAIN_ID]), // CHAIN_ID: 8 bytes
+      encodePacked(['uint8'], [1]),                 // VERSION: 1 byte
+      encodePacked(['uint8'], [role]),              // ROLE: 1 byte
+      rlpTxList                                     // RLP_TX_LIST
+    ];
+
+    if (signature) {
+      parts.push(signature);
+    }
+
+    const contentData = concatHex(parts);
+
+    const hash = keccak256(contentData);
+    return Buffer.from(hash.slice(2), 'hex');
+  }
+  
+  private async calculateGasBid(): Promise {
+    // Get actual gas prices from L1
+    const fees = await this.l1Client.estimateFeesPerGas();
+    // Use 2x the current base fee for reliability
+    return fees.maxFeePerGas ? fees.maxFeePerGas * 2n : 100000000000n;
+  }
+  
+  async shouldCreateBatch(): Promise {
+    const database = this.db.getDatabase();
+    
+    const stats = database.prepare(`
+      SELECT 
+        COUNT(*) as pending_count,
+        SUM(LENGTH(raw)) as pending_size
+      FROM transactions 
+      WHERE state IN ('queued', 'requeued')
+    `).get() as { pending_count: number; pending_size: number | null };
+    
+    if (stats.pending_count === 0) return false;
+    
+    const timeSinceLastBatch = Date.now() - this.lastBatchTime;
+    const pendingSize = stats.pending_size || 0;
+    
+    // Dynamic triggers
+    const shouldBatch = 
+      pendingSize >= (this.MAX_BLOB_SIZE - 1000) ||
+      stats.pending_count >= this.getOptimalBatchSize() ||
+      (stats.pending_count > 0 && timeSinceLastBatch >= 3000);
+    
+    if (shouldBatch) {
+      this.lastBatchTime = Date.now();
+    }
+    
+    return shouldBatch;
+  }
+  
+  private getOptimalBatchSize(): number {
+    // In production, adjust based on L1 congestion
+    return 200;
+  }
+}
diff --git a/sequencer/src/config/config.ts b/sequencer/src/config/config.ts
new file mode 100644
index 00000000..85edeea4
--- /dev/null
+++ b/sequencer/src/config/config.ts
@@ -0,0 +1,108 @@
+import { config as dotenvConfig } from 'dotenv';
+import { resolve } from 'path';
+import type { Hex } from 'viem';
+
+// Load environment variables
+dotenvConfig({ path: resolve(process.cwd(), '.env') });
+
+export interface Config {
+  // L1 Connection
+  l1RpcUrl: string;
+  l1ChainId: number;
+  privateKey: Hex;
+
+  // L2 Connection
+  l2RpcUrl: string;
+  l2ChainId?: string; // Optional - will be discovered from L2 RPC if not provided
+
+  // Facet Configuration
+  facetMagicPrefix: Hex;
+
+  // Batching Parameters
+  maxTxPerBatch: number;
+  maxBatchSize: number;
+  batchIntervalMs: number;
+  maxPerSender: number;
+
+  // Economics
+  minGasPrice: bigint;
+  baseFeeMultiplier: number;
+  escalationRate: number;
+
+  // Operational
+  maxPendingTxs: number;
+  dbPath: string;
+  port: number;
+  logLevel: string;
+
+  // Monitoring
+  metricsEnabled: boolean;
+  metricsPort: number;
+
+  // DA Builder Configuration
+  useDABuilder: boolean;
+  daBuilderUrl?: string;
+  proposerAddress?: Hex;
+  fallbackToDirect: boolean;
+}
+
+export function loadConfig(): Config {
+  const config: Config = {
+    // L1 Connection
+    l1RpcUrl: process.env.L1_RPC_URL!,
+    l1ChainId: parseInt(process.env.L1_CHAIN_ID!, 16), // Holesky
+    privateKey: (process.env.PRIVATE_KEY || '0x') as Hex,
+    
+    // L2 Connection
+    l2RpcUrl: process.env.L2_RPC_URL || 'http://localhost:8546',
+    l2ChainId: process.env.L2_CHAIN_ID, // Optional - discovered from L2 RPC if not set
+    
+    // Facet Configuration
+    facetMagicPrefix: process.env.FACET_MAGIC_PREFIX as Hex,
+    
+    // Batching Parameters
+    maxTxPerBatch: parseInt(process.env.MAX_TX_PER_BATCH || '500'),
+    maxBatchSize: parseInt(process.env.MAX_BATCH_SIZE || '131072'),
+    batchIntervalMs: parseInt(process.env.BATCH_INTERVAL_MS || '3000'),
+    maxPerSender: parseInt(process.env.MAX_PER_SENDER || '10'),
+    
+    // Economics
+    minGasPrice: BigInt(process.env.MIN_GAS_PRICE || '1000000000'),
+    baseFeeMultiplier: parseFloat(process.env.BASE_FEE_MULTIPLIER || '2'),
+    escalationRate: parseFloat(process.env.ESCALATION_RATE || '1.125'),
+    
+    // Operational
+    maxPendingTxs: parseInt(process.env.MAX_PENDING_TXS || '10000'),
+    dbPath: process.env.DB_PATH || './data/sequencer.db',
+    port: parseInt(process.env.PORT || '8547'),
+    logLevel: process.env.LOG_LEVEL || 'info',
+    
+    // Monitoring
+    metricsEnabled: process.env.METRICS_ENABLED === 'true',
+    metricsPort: parseInt(process.env.METRICS_PORT || '9090'),
+
+    // DA Builder Configuration
+    useDABuilder: process.env.USE_DA_BUILDER === 'true',
+    daBuilderUrl: process.env.DA_BUILDER_URL,
+    proposerAddress: process.env.PROPOSER_ADDRESS as Hex | undefined,
+    fallbackToDirect: process.env.FALLBACK_TO_DIRECT !== 'false' // Default true
+  };
+  
+  // Validate required config
+  if (!config.privateKey || config.privateKey === '0x') {
+    throw new Error('PRIVATE_KEY is required');
+  }
+
+  if (!config.l1RpcUrl) {
+    throw new Error('L1_RPC_URL is required');
+  }
+
+  // Validate DA Builder config if enabled
+  if (config.useDABuilder) {
+    if (!config.daBuilderUrl) {
+      throw new Error('DA_BUILDER_URL is required when USE_DA_BUILDER is true');
+    }
+  }
+
+  return config;
+}
\ No newline at end of file
diff --git a/sequencer/src/db/schema.ts b/sequencer/src/db/schema.ts
new file mode 100644
index 00000000..2eb44865
--- /dev/null
+++ b/sequencer/src/db/schema.ts
@@ -0,0 +1,178 @@
+import Database from 'better-sqlite3';
+import { type Hex } from 'viem';
+
+export interface Transaction {
+  hash: Buffer;
+  raw: Buffer;
+  from_address: Buffer;
+  nonce: number;
+  max_fee_per_gas: string;
+  max_priority_fee_per_gas?: string;  // Optional for legacy transactions
+  gas_limit: number;
+  intrinsic_gas: number;
+  received_seq: number;
+  received_at: number;
+  state: 'queued' | 'batched' | 'submitted' | 'l1_included' | 'l2_included' | 'dropped' | 'requeued';
+  batch_id?: number;
+  l2_block_number?: number;
+  drop_reason?: string;
+}
+
+export interface Batch {
+  id: number;
+  content_hash: Buffer;
+  wire_format: Buffer;
+  state: 'open' | 'sealed' | 'submitted' | 'l1_included' | 'reorged' | 'failed' | 'finalized';
+  sealed_at?: number;
+  blob_size: number;
+  gas_bid: string;
+  tx_count: number;
+  tx_hashes: string; // JSON array of transaction hashes
+}
+
+// Removed BatchItem interface - now using JSON column in batches table
+
+export interface PostAttempt {
+  id: number;
+  batch_id: number;
+  l1_tx_hash?: Buffer;
+  da_builder_request_id?: string;
+  l1_nonce?: number;
+  gas_price: string;
+  max_fee_per_gas?: string;
+  max_fee_per_blob_gas?: string;
+  submitted_at: number;
+  confirmed_at?: number;
+  block_number?: number;
+  block_hash?: Buffer;
+  status: 'pending' | 'mined' | 'replaced' | 'reorged' | 'failed';
+  replaced_by?: number;
+  failure_reason?: string;
+}
+
+export const createSchema = (db: Database.Database) => {
+  db.pragma('journal_mode = WAL');
+  db.pragma('busy_timeout = 5000');
+  
+  db.exec(`
+    -- Transaction state machine
+    CREATE TABLE IF NOT EXISTS transactions (
+      hash BLOB PRIMARY KEY,
+      raw BLOB NOT NULL,
+      from_address BLOB NOT NULL,
+      nonce INTEGER NOT NULL,
+      max_fee_per_gas TEXT NOT NULL,
+      max_priority_fee_per_gas TEXT,  -- Nullable for legacy transactions
+      gas_limit INTEGER NOT NULL,
+      intrinsic_gas INTEGER NOT NULL,
+      received_seq INTEGER NOT NULL,
+      received_at INTEGER NOT NULL,
+      state TEXT NOT NULL DEFAULT 'queued' CHECK(state IN (
+        'queued', 'batched', 'submitted', 'l1_included', 'l2_included', 'dropped', 'requeued'
+      )),
+      batch_id INTEGER,
+      l2_block_number INTEGER,
+      drop_reason TEXT,
+      FOREIGN KEY (batch_id) REFERENCES batches(id)
+    );
+    
+    -- Batch state machine
+    CREATE TABLE IF NOT EXISTS batches (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      content_hash BLOB NOT NULL UNIQUE,
+      wire_format BLOB NOT NULL,
+      state TEXT NOT NULL DEFAULT 'open' CHECK(state IN (
+        'open', 'sealed', 'submitted', 'l1_included', 'reorged', 'failed', 'finalized'
+      )),
+      sealed_at INTEGER,
+      blob_size INTEGER NOT NULL,
+      gas_bid TEXT NOT NULL,
+      tx_count INTEGER NOT NULL,
+      tx_hashes JSON NOT NULL DEFAULT '[]' -- JSON array of transaction hashes in order
+    );
+    
+    -- Tracks all L1 submission attempts
+    CREATE TABLE IF NOT EXISTS post_attempts (
+      id INTEGER PRIMARY KEY AUTOINCREMENT,
+      batch_id INTEGER NOT NULL,
+      l1_tx_hash BLOB,
+      da_builder_request_id TEXT,
+      l1_nonce INTEGER,
+      gas_price TEXT NOT NULL,
+      max_fee_per_gas TEXT,
+      max_fee_per_blob_gas TEXT,
+      submitted_at INTEGER NOT NULL,
+      confirmed_at INTEGER,
+      block_number INTEGER,
+      block_hash BLOB,
+      status TEXT NOT NULL DEFAULT 'pending' CHECK(status IN (
+        'pending', 'mined', 'replaced', 'reorged', 'failed'
+      )),
+      replaced_by INTEGER,
+      failure_reason TEXT,
+      FOREIGN KEY (batch_id) REFERENCES batches(id),
+      FOREIGN KEY (replaced_by) REFERENCES post_attempts(id)
+    );
+    
+    -- Critical indexes for performance
+    CREATE INDEX IF NOT EXISTS idx_tx_state_queued 
+      ON transactions(state, max_fee_per_gas DESC, received_seq ASC) 
+      WHERE state IN ('queued', 'requeued');
+    CREATE INDEX IF NOT EXISTS idx_tx_from_nonce 
+      ON transactions(from_address, nonce);
+    CREATE INDEX IF NOT EXISTS idx_batch_state 
+      ON batches(state) WHERE state IN ('sealed', 'submitted');
+    CREATE INDEX IF NOT EXISTS idx_batch_content_hash 
+      ON batches(content_hash);
+    CREATE INDEX IF NOT EXISTS idx_attempts_pending 
+      ON post_attempts(status, submitted_at) WHERE status = 'pending';
+    CREATE INDEX IF NOT EXISTS idx_attempts_batch 
+      ON post_attempts(batch_id, status);
+  `);
+};
+
+export class DatabaseService {
+  private db: Database.Database;
+  
+  constructor(dbPath: string) {
+    this.db = new Database(dbPath);
+    createSchema(this.db);
+    
+    // Prepare common statements
+    this.insertTx = this.db.prepare(`
+      INSERT INTO transactions (
+        hash, raw, from_address, nonce, max_fee_per_gas,
+        max_priority_fee_per_gas, gas_limit, intrinsic_gas,
+        received_seq, received_at, state
+      ) VALUES (
+        @hash, @raw, @from_address, @nonce, @max_fee_per_gas,
+        @max_priority_fee_per_gas, @gas_limit, @intrinsic_gas,
+        @received_seq, @received_at, @state
+      )
+    `);
+    
+    this.getQueuedCount = this.db.prepare(`
+      SELECT COUNT(*) as count FROM transactions 
+      WHERE state IN ('queued', 'requeued')
+    `);
+    
+    this.getQueuedTxs = this.db.prepare(`
+      SELECT * FROM transactions 
+      WHERE state IN ('queued', 'requeued')
+      ORDER BY max_fee_per_gas DESC, received_seq ASC
+      LIMIT ?
+    `);
+  }
+  
+  private insertTx: Database.Statement;
+  private getQueuedCount: Database.Statement;
+  private getQueuedTxs: Database.Statement;
+  
+  getDatabase(): Database.Database {
+    return this.db;
+  }
+  
+  close(): void {
+    this.db.close();
+  }
+}
\ No newline at end of file
diff --git a/sequencer/src/index.ts b/sequencer/src/index.ts
new file mode 100644
index 00000000..341cada2
--- /dev/null
+++ b/sequencer/src/index.ts
@@ -0,0 +1,241 @@
+import { DatabaseService } from './db/schema.js';
+import { SequencerAPI } from './server/api.js';
+import { BatchMaker } from './batch/maker.js';
+import type { Poster } from './l1/poster-interface.js';
+import { DirectPoster } from './l1/direct-poster.js';
+import { DABuilderPoster } from './l1/da-builder-poster.js';
+import { InclusionMonitor } from './l1/monitor.js';
+import { loadConfig } from './config/config.js';
+import { logger } from './utils/logger.js';
+import { defineChain, createPublicClient, http } from 'viem';
+import { holesky, mainnet } from 'viem/chains';
+import { mkdir } from 'fs/promises';
+import { dirname } from 'path';
+
+class Sequencer {
+  private db!: DatabaseService;
+  private api!: SequencerAPI;
+  private batchMaker!: BatchMaker;
+  private poster!: Poster;
+  private monitor!: InclusionMonitor;
+  private config = loadConfig();
+  private isRunning = false;
+  private batchInterval?: NodeJS.Timeout;
+  private posterInterval?: NodeJS.Timeout;
+  
+  async init(): Promise {
+    logger.info('Initializing sequencer...');
+    
+    // Ensure data directory exists
+    await mkdir(dirname(this.config.dbPath), { recursive: true });
+    
+    // Initialize database
+    this.db = new DatabaseService(this.config.dbPath);
+    
+    // Determine L1 chain
+    let l1Chain;
+    if (this.config.l1ChainId === 1) {
+      l1Chain = mainnet;
+    } else if (this.config.l1ChainId === 17000) {
+      l1Chain = holesky;
+    } else if (this.config.l1ChainId === 560048) {
+      // Define Hoodi chain
+      l1Chain = defineChain({
+        id: 560048,
+        name: 'Hoodi',
+        nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+        rpcUrls: {
+          default: { http: [this.config.l1RpcUrl] }
+        }
+      });
+    } else {
+      // Define custom chain
+      l1Chain = defineChain({
+        id: this.config.l1ChainId,
+        name: 'Custom L1',
+        nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+        rpcUrls: {
+          default: { http: [this.config.l1RpcUrl] }
+        }
+      });
+    }
+    
+    // Query L2 chain ID from Geth
+    const l2Client = createPublicClient({
+      transport: http(this.config.l2RpcUrl)
+    });
+    const l2ChainId = await l2Client.getChainId();
+    logger.info({ l2ChainId }, 'Discovered L2 chain ID from Geth');
+
+    // Define L2 chain
+    const l2Chain = defineChain({
+      id: l2ChainId,
+      name: 'Facet',
+      nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
+      rpcUrls: {
+        default: { http: [this.config.l2RpcUrl] }
+      }
+    });
+
+    // Create L1 public client for BatchMaker
+    const l1PublicClient = createPublicClient({
+      chain: l1Chain,
+      transport: http(this.config.l1RpcUrl)
+    });
+
+    // Initialize components
+    this.api = new SequencerAPI(this.db, this.config);
+    this.batchMaker = new BatchMaker(
+      this.db,
+      l1PublicClient,
+      l2ChainId.toString()
+    );
+
+    // Select poster implementation based on config
+    if (this.config.useDABuilder) {
+      logger.info('Using DA Builder poster');
+      this.poster = new DABuilderPoster(
+        this.db,
+        l1Chain,
+        this.config.privateKey,
+        this.config.l1RpcUrl,
+        this.config.daBuilderUrl!
+      );
+    } else {
+      logger.info('Using direct poster');
+      this.poster = new DirectPoster(
+        this.db,
+        l1Chain,
+        this.config.privateKey,
+        this.config.l1RpcUrl
+      );
+    }
+    this.monitor = new InclusionMonitor(
+      this.db,
+      this.config.l1RpcUrl,
+      this.config.l2RpcUrl,
+      l1Chain,
+      l2Chain
+    );
+    
+    logger.info('Sequencer initialized');
+  }
+  
+  async start(): Promise {
+    if (this.isRunning) return;
+    this.isRunning = true;
+    
+    logger.info('Starting sequencer...');
+    
+    // Start API server
+    await this.api.start();
+    
+    // Start inclusion monitor
+    await this.monitor.start();
+    
+    // Start batch creation loop
+    this.batchInterval = setInterval(async () => {
+      try {
+        if (await this.batchMaker.shouldCreateBatch()) {
+          const batchId = await this.batchMaker.createBatch();
+          if (batchId) {
+            logger.info({ batchId }, 'Created new batch');
+            // Post immediately
+            await this.poster.postBatch(batchId);
+          }
+        }
+      } catch (error: any) {
+        logger.error({ error: error.message }, 'Error in batch creation loop');
+      }
+    }, this.config.batchIntervalMs);
+    
+    // Start poster check loop (for RBF)
+    this.posterInterval = setInterval(async () => {
+      try {
+        await this.poster.checkPendingTransaction();
+        
+        // Also check for any sealed batches that need posting
+        const database = this.db.getDatabase();
+        const sealedBatches = database.prepare(
+          'SELECT id FROM batches WHERE state = ? LIMIT 1'
+        ).all('sealed') as Array<{ id: number }>;
+        
+        for (const batch of sealedBatches) {
+          await this.poster.postBatch(batch.id);
+        }
+      } catch (error: any) {
+        logger.error({ error: error.message }, 'Error in poster check loop');
+      }
+    }, 10000); // Check every 10 seconds
+    
+    logger.info('Sequencer started successfully');
+    
+    // Log initial stats
+    const stats = await this.getStats();
+    logger.info(stats, 'Initial stats');
+  }
+  
+  async stop(): Promise {
+    if (!this.isRunning) return;
+    
+    logger.info('Stopping sequencer...');
+    
+    // Stop intervals
+    if (this.batchInterval) clearInterval(this.batchInterval);
+    if (this.posterInterval) clearInterval(this.posterInterval);
+    
+    // Stop API
+    await this.api.stop();
+    
+    // Close database
+    this.db.close();
+    
+    this.isRunning = false;
+    logger.info('Sequencer stopped');
+  }
+  
+  private async getStats(): Promise {
+    const database = this.db.getDatabase();
+    return database.prepare(`
+      SELECT 
+        (SELECT COUNT(*) FROM transactions) as total_txs,
+        (SELECT COUNT(*) FROM transactions WHERE state = 'queued') as queued_txs,
+        (SELECT COUNT(*) FROM batches) as total_batches,
+        (SELECT COUNT(*) FROM batches WHERE state = 'l1_included') as confirmed_batches
+    `).get();
+  }
+}
+
+// Main entry point
+async function main() {
+  const sequencer = new Sequencer();
+  
+  try {
+    await sequencer.init();
+    await sequencer.start();
+    
+    // Handle graceful shutdown
+    process.on('SIGINT', async () => {
+      logger.info('Received SIGINT, shutting down gracefully...');
+      await sequencer.stop();
+      process.exit(0);
+    });
+    
+    process.on('SIGTERM', async () => {
+      logger.info('Received SIGTERM, shutting down gracefully...');
+      await sequencer.stop();
+      process.exit(0);
+    });
+    
+  } catch (error: any) {
+    logger.error({ error: error.message }, 'Failed to start sequencer');
+    process.exit(1);
+  }
+}
+
+// Run if this is the main module
+if (import.meta.url === `file://${process.argv[1]}`) {
+  main().catch(console.error);
+}
+
+export { Sequencer };
\ No newline at end of file
diff --git a/sequencer/src/l1/da-builder-client.ts b/sequencer/src/l1/da-builder-client.ts
new file mode 100644
index 00000000..eae80ac5
--- /dev/null
+++ b/sequencer/src/l1/da-builder-client.ts
@@ -0,0 +1,238 @@
+import {
+  type Hex,
+  type PrivateKeyAccount,
+  type PublicClient,
+  type WalletClient,
+  toHex,
+  parseAbi,
+  encodeAbiParameters,
+  parseAbiParameters,
+  encodeFunctionData,
+  toBlobs,
+  createWalletClient,
+  http
+} from 'viem';
+import { logger } from '../utils/logger.js';
+
+export interface DABuilderSubmitResult {
+  id: string;  // Request ID from DA Builder
+}
+
+export interface DABuilderReceiptResult {
+  txHash: Hex;
+  blockNumber: bigint;
+  blockHash: Hex;
+}
+
+export class DABuilderClient {
+  private walletClient: WalletClient;
+  private kzg: any;
+
+  constructor(
+    private daBuilderUrl: string,
+    private chainId: number,
+    private account: PrivateKeyAccount,
+    private publicClient: PublicClient
+  ) {
+    // Create wallet client for DA Builder
+    this.walletClient = createWalletClient({
+      account: this.account,
+      chain: { id: this.chainId, name: 'Custom', nativeCurrency: { name: 'ETH', symbol: 'ETH', decimals: 18 }, rpcUrls: { default: { http: [daBuilderUrl] } } },
+      transport: http(daBuilderUrl)
+    });
+  }
+
+  async initKzg() {
+    try {
+      const cKzg = await import('c-kzg');
+      cKzg.default.loadTrustedSetup(0);
+      this.kzg = cKzg.default;
+    } catch (error: any) {
+      logger.error({ error: error.message }, 'Failed to initialize KZG for DA Builder');
+      throw error;
+    }
+  }
+
+  /**
+   * Submit blob data to DA Builder
+   */
+  async submit(blobData: Hex): Promise {
+    try {
+      // Initialize KZG if not ready
+      if (!this.kzg) {
+        await this.initKzg();
+      }
+
+      // Prepare the EIP-712 signed call
+      const onCallData = await this.prepareEIP712Call(blobData);
+
+      // Convert data to blobs
+      const blobs = toBlobs({ data: blobData });
+
+      // Get current nonce and add random offset to avoid duplicates
+      // DA Builder doesn't actually use this nonce on-chain, but needs unique transactions
+      const currentNonce = await this.publicClient.getTransactionCount({
+        address: this.account.address,
+        blockTag: 'latest'
+      });
+      const nonce = currentNonce + (Math.floor(Math.random() * 1000) + 100);
+
+      // Get gas prices
+      const fees = await this.publicClient.estimateFeesPerGas();
+      const blobBaseFee = await this.publicClient.getBlobBaseFee();
+
+      // Always target the next block for DA Builder submission
+      const currentBlock = await this.publicClient.getBlockNumber();
+      const targetBlockNumber = currentBlock + 1n;
+
+      // Sign the transaction
+      const signedTx = await this.account.signTransaction({
+        to: this.account.address, // EOA with 7702 code
+        data: onCallData,
+        blobs,
+        kzg: this.kzg,
+        nonce,
+        gas: 500000n,
+        maxPriorityFeePerGas: fees.maxPriorityFeePerGas! * 2n,
+        maxFeePerGas: fees.maxFeePerGas! * 2n,
+        maxFeePerBlobGas: blobBaseFee * 2n > 5000000000n ? blobBaseFee * 2n : 5000000000n, // Min 5 gwei
+        type: 'eip4844',
+        chainId: this.chainId
+      });
+
+      // Submit via eth_sendBundle
+      const response = await fetch(this.daBuilderUrl, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          jsonrpc: '2.0',
+          id: 1,
+          method: 'eth_sendBundle',
+          params: [{
+            txs: [signedTx],  // Array with single serialized transaction
+            blockNumber: `0x${targetBlockNumber.toString(16)}`  // Block number as hex string
+          }]
+        })
+      });
+
+      const result: any = await response.json();
+
+      if (result.error) {
+        throw new Error(result.error.message || 'DA Builder returned error');
+      }
+
+      const requestId = result.result;
+      logger.info({ requestId, targetBlock: targetBlockNumber }, 'Submitted bundle to DA Builder');
+
+      return { id: requestId };
+    } catch (error: any) {
+      logger.error({ error: error.message }, 'Failed to submit to DA Builder');
+      throw error;
+    }
+  }
+
+  /**
+   * Poll DA Builder for transaction receipt
+   */
+  async poll(requestId: string): Promise {
+    try {
+      const response = await fetch(this.daBuilderUrl, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          jsonrpc: '2.0',
+          id: 1,
+          method: 'eth_getTransactionReceipt',
+          params: [requestId]
+        })
+      });
+
+      const result: any = await response.json();
+
+      if (result.result) {
+        return {
+          txHash: result.result.transactionHash,
+          blockNumber: BigInt(result.result.blockNumber),
+          blockHash: result.result.blockHash
+        };
+      }
+
+      return null; // Still pending
+    } catch (error: any) {
+      logger.debug({ error: error.message }, 'Error polling DA Builder');
+      return null;
+    }
+  }
+
+  /**
+   * Prepare EIP-712 signed call for TrustlessProposer
+   */
+  private async prepareEIP712Call(blobData: Hex): Promise {
+    // Get nested nonce from TrustlessProposer contract
+    const proposerAbi = parseAbi([
+      'function nestedNonce() view returns (uint256)'
+    ]);
+
+    const nonce = await this.publicClient.readContract({
+      address: this.account.address, // EOA with 7702 code
+      abi: proposerAbi,
+      functionName: 'nestedNonce'
+    });
+
+    // Set deadline to 5 minutes from now
+    const deadline = BigInt(Math.floor(Date.now() / 1000) + 300);
+
+    // Create EIP-712 domain
+    const domain = {
+      name: 'TrustlessProposer',
+      version: '1',
+      chainId: BigInt(this.chainId),
+      verifyingContract: this.account.address // EOA with 7702 code
+    };
+
+    // EIP-712 types
+    const types = {
+      Call: [
+        { name: 'deadline', type: 'uint256' },
+        { name: 'nonce', type: 'uint256' },
+        { name: 'target', type: 'address' },
+        { name: 'value', type: 'uint256' },
+        { name: 'calldata', type: 'bytes' },
+        { name: 'gasLimit', type: 'uint256' }
+      ]
+    } as const;
+
+    // Message to sign
+    const message = {
+      deadline,
+      nonce,
+      target: '0x0000000000000000000000000000000000000000' as Hex, // Dummy target for blob data
+      value: 0n,
+      calldata: blobData,
+      gasLimit: 500000n
+    };
+
+    // Sign the message
+    const signature = await this.account.signTypedData({
+      domain,
+      types,
+      primaryType: 'Call',
+      message
+    });
+
+    // Encode the onCall parameters
+    const encodedCall = encodeAbiParameters(
+      parseAbiParameters('bytes, uint256, uint256, bytes, uint256'),
+      [signature, deadline, nonce, blobData, 500000n]
+    );
+
+    // Encode the onCall function call
+    const onCallData = encodeFunctionData({
+      abi: parseAbi(['function onCall(address target, bytes calldata data, uint256 value) returns (bool)']),
+      functionName: 'onCall',
+      args: ['0x0000000000000000000000000000000000000000' as Hex, encodedCall, 0n]
+    });
+
+    return onCallData;
+  }
+}
\ No newline at end of file
diff --git a/sequencer/src/l1/da-builder-poster.ts b/sequencer/src/l1/da-builder-poster.ts
new file mode 100644
index 00000000..3835a694
--- /dev/null
+++ b/sequencer/src/l1/da-builder-poster.ts
@@ -0,0 +1,177 @@
+import {
+  createPublicClient,
+  http,
+  type Hex,
+  type PrivateKeyAccount,
+  type PublicClient,
+  type Chain
+} from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+import type { DatabaseService, Batch } from '../db/schema.js';
+import { logger } from '../utils/logger.js';
+import type { Poster, PendingTransaction } from './poster-interface.js';
+import { DABuilderClient } from './da-builder-client.js';
+
+export class DABuilderPoster implements Poster {
+  private publicClient: PublicClient;
+  private account: PrivateKeyAccount;
+  private daBuilderClient: DABuilderClient;
+  private currentPending: PendingTransaction | null = null;
+  private lastPollTime: number = 0;
+  private pollInterval: number = 5000; // Poll every 5 seconds
+
+  constructor(
+    private db: DatabaseService,
+    private chain: Chain,
+    privateKey: Hex,
+    rpcUrl: string,
+    daBuilderUrl: string
+  ) {
+    this.account = privateKeyToAccount(privateKey);
+
+    this.publicClient = createPublicClient({
+      chain: this.chain,
+      transport: http(rpcUrl)
+    });
+
+    this.daBuilderClient = new DABuilderClient(
+      daBuilderUrl,
+      this.chain.id,
+      this.account,
+      this.publicClient
+    );
+  }
+
+  async postBatch(batchId: number): Promise {
+    const database = this.db.getDatabase();
+
+    // Get batch data
+    const batch = database.prepare(
+      'SELECT * FROM batches WHERE id = ? AND state = ?'
+    ).get(batchId, 'sealed') as Batch | undefined;
+
+    if (!batch) {
+      logger.error({ batchId }, 'Batch not found or not sealed');
+      return;
+    }
+
+    try {
+      // Convert wire format to hex
+      const wireFormatHex = ('0x' + batch.wire_format.toString('hex')) as Hex;
+
+      // Submit to DA Builder - let it determine the next available block
+      const submitResult = await this.daBuilderClient.submit(wireFormatHex);
+
+      // Track pending transaction
+      this.currentPending = {
+        batchId,
+        requestId: submitResult.id,
+        submittedAt: Date.now(),
+        attempts: 1
+      };
+
+      // Store post attempt with DA Builder request ID
+      // Need to provide all required columns even if not used for DA Builder
+      database.prepare(`
+        INSERT INTO post_attempts (
+          batch_id, da_builder_request_id, l1_nonce, gas_price,
+          max_fee_per_gas, max_fee_per_blob_gas, submitted_at, status
+        ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
+      `).run(
+        batchId,
+        submitResult.id,
+        0, // nonce not used for DA Builder
+        '0', // gas prices handled by DA Builder
+        '0',
+        '0',
+        Date.now()
+      );
+
+      // Update batch state
+      database.prepare(
+        'UPDATE batches SET state = ? WHERE id = ?'
+      ).run('submitted', batchId);
+
+      logger.info({
+        batchId,
+        requestId: submitResult.id
+      }, 'Batch submitted to DA Builder');
+
+    } catch (error: any) {
+      logger.error({ batchId, error: error.message }, 'Failed to submit batch to DA Builder');
+
+      // Check if we should fallback to direct submission
+      // This would be handled by the orchestrator based on config.fallbackToDirect
+      throw error;
+    }
+  }
+
+  async checkPendingTransaction(): Promise {
+    if (!this.currentPending || !this.currentPending.requestId) return;
+
+    // Rate limit polling
+    const now = Date.now();
+    if (now - this.lastPollTime < this.pollInterval) return;
+    this.lastPollTime = now;
+
+    try {
+      // Poll DA Builder for receipt
+      const receipt = await this.daBuilderClient.poll(this.currentPending.requestId);
+
+      if (receipt) {
+        const database = this.db.getDatabase();
+
+        // Update post attempt with L1 tx hash
+        database.prepare(`
+          UPDATE post_attempts
+          SET l1_tx_hash = ?, status = 'mined', confirmed_at = ?
+          WHERE da_builder_request_id = ?
+        `).run(
+          Buffer.from(receipt.txHash.slice(2), 'hex'),
+          Date.now(),
+          this.currentPending.requestId
+        );
+
+        logger.info({
+          batchId: this.currentPending.batchId,
+          requestId: this.currentPending.requestId,
+          txHash: receipt.txHash,
+          blockNumber: receipt.blockNumber
+        }, 'DA Builder transaction confirmed');
+
+        // Clear current pending
+        this.currentPending = null;
+      } else {
+        // Still pending, check if timeout
+        const elapsed = Date.now() - this.currentPending.submittedAt;
+        if (elapsed > 900000) { // 15 minutes timeout
+          logger.warn({
+            batchId: this.currentPending.batchId,
+            requestId: this.currentPending.requestId,
+            elapsed
+          }, 'DA Builder submission timeout');
+
+          // Mark as failed
+          const database = this.db.getDatabase();
+          database.prepare(`
+            UPDATE post_attempts
+            SET status = 'failed'
+            WHERE da_builder_request_id = ?
+          `).run(this.currentPending.requestId);
+
+          // Clear and let orchestrator handle retry
+          this.currentPending = null;
+        }
+      }
+    } catch (error: any) {
+      logger.debug({
+        error: error.message,
+        requestId: this.currentPending?.requestId
+      }, 'Error checking DA Builder status');
+    }
+  }
+
+  getPendingTransaction(): PendingTransaction | null {
+    return this.currentPending;
+  }
+}
\ No newline at end of file
diff --git a/sequencer/src/l1/direct-poster.ts b/sequencer/src/l1/direct-poster.ts
new file mode 100644
index 00000000..ef31f228
--- /dev/null
+++ b/sequencer/src/l1/direct-poster.ts
@@ -0,0 +1,265 @@
+import {
+  createWalletClient,
+  createPublicClient,
+  http,
+  toBlobs,
+  type WalletClient,
+  type PublicClient,
+  type Hex,
+  type PrivateKeyAccount,
+  type Chain
+} from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+import type { DatabaseService, Batch } from '../db/schema.js';
+import { logger } from '../utils/logger.js';
+import type { Poster, PendingTransaction } from './poster-interface.js';
+
+export class DirectPoster implements Poster {
+  private wallet: WalletClient;
+  private publicClient: PublicClient;
+  private account: PrivateKeyAccount;
+  private currentBlobTx: PendingTransaction | null = null;
+  private lastNonce: number = 0;
+  private kzg: any;
+  private kzgReady: Promise;
+
+  constructor(
+    private db: DatabaseService,
+    private chain: Chain,
+    privateKey: Hex,
+    rpcUrl: string
+  ) {
+    this.account = privateKeyToAccount(privateKey);
+
+    this.wallet = createWalletClient({
+      account: this.account,
+      chain: this.chain,
+      transport: http(rpcUrl)
+    });
+
+    this.publicClient = createPublicClient({
+      chain: this.chain,
+      transport: http(rpcUrl)
+    });
+
+    // Initialize KZG
+    this.kzgReady = this.initKzg();
+  }
+
+  private async initKzg() {
+    try {
+      const cKzg = await import('c-kzg');
+      // c-kzg 4.x includes the trusted setup internally
+      // Just pass the preset id (0 for mainnet)
+      cKzg.default.loadTrustedSetup(0);
+      this.kzg = cKzg.default;
+      logger.info('KZG initialized successfully');
+    } catch (error: any) {
+      logger.warn({ error: error.message }, 'KZG initialization failed, blob transactions may not work');
+      // Try without any parameters as a fallback
+      try {
+        const cKzg = await import('c-kzg');
+        this.kzg = cKzg.default;
+        logger.info('KZG initialized without explicit trusted setup loading');
+      } catch (e) {
+        logger.error('Failed to initialize KZG completely');
+      }
+    }
+  }
+
+  async postBatch(batchId: number): Promise {
+    // Wait for KZG to be ready
+    await this.kzgReady;
+
+    const database = this.db.getDatabase();
+
+    // Get batch data
+    const batch = database.prepare(
+      'SELECT * FROM batches WHERE id = ? AND state = ?'
+    ).get(batchId, 'sealed') as Batch | undefined;
+
+    if (!batch) {
+      logger.error({ batchId }, 'Batch not found or not sealed');
+      return;
+    }
+
+    // Check if previous tx confirmed
+    const currentNonce = await this.publicClient.getTransactionCount({
+      address: this.account.address,
+      blockTag: 'latest'
+    });
+
+    if (currentNonce > this.lastNonce) {
+      // Previous confirmed, start fresh
+      this.currentBlobTx = null;
+      this.lastNonce = currentNonce;
+    }
+
+    // Prepare blob transaction
+    const blobTx = this.currentBlobTx ?
+      await this.createReplacementTx(batch) :
+      await this.createNewTx(batch);
+
+    try {
+      // Convert wire format to blobs
+      const wireFormatHex = ('0x' + batch.wire_format.toString('hex')) as Hex;
+      const blobs = toBlobs({ data: wireFormatHex });
+
+      // Check if KZG is available
+      if (!this.kzg) {
+        throw new Error('KZG not initialized - cannot send blob transaction');
+      }
+
+      // Submit transaction
+      const txHash = await this.wallet.sendTransaction({
+        account: this.account,
+        chain: this.chain,
+        blobs,
+        kzg: this.kzg, // Pass the KZG instance
+        to: '0x0000000000000000000000000000000000000000' as Hex, // Burn address for blobs
+        nonce: this.currentBlobTx?.nonce || currentNonce,
+        gas: 100000n,
+        maxFeePerGas: blobTx.maxFeePerGas,
+        maxPriorityFeePerGas: blobTx.maxPriorityFeePerGas,
+        maxFeePerBlobGas: blobTx.maxFeePerBlobGas,
+        type: 'eip4844'
+      });
+
+      // Track for monitoring - make sure we store the actual fees used
+      this.currentBlobTx = {
+        batchId,
+        txHash,
+        nonce: this.currentBlobTx?.nonce || currentNonce,
+        gasPrice: blobTx.maxFeePerGas,  // Store the actual fee we just used
+        blobGasPrice: blobTx.maxFeePerBlobGas,
+        submittedAt: Date.now(),
+        attempts: (this.currentBlobTx?.attempts || 0) + 1
+      };
+
+      // Store post attempt
+      database.prepare(`
+        INSERT INTO post_attempts (
+          batch_id, l1_tx_hash, l1_nonce, gas_price,
+          max_fee_per_gas, max_fee_per_blob_gas, submitted_at, status
+        ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
+      `).run(
+        batchId,
+        Buffer.from(txHash.slice(2), 'hex'),
+        this.currentBlobTx.nonce,
+        blobTx.maxFeePerGas.toString(),
+        blobTx.maxFeePerGas.toString(),
+        blobTx.maxFeePerBlobGas.toString(),
+        Date.now()
+      );
+
+      // Update batch state
+      database.prepare(
+        'UPDATE batches SET state = ? WHERE id = ?'
+      ).run('submitted', batchId);
+
+      logger.info({
+        batchId,
+        txHash,
+        nonce: this.currentBlobTx.nonce,
+        attempt: this.currentBlobTx.attempts
+      }, 'Batch submitted to L1');
+
+    } catch (error: any) {
+      logger.error({ batchId, error: error.message }, 'Failed to submit batch');
+      this.handleSubmissionError(error, batchId);
+    }
+  }
+
+  async checkPendingTransaction(): Promise {
+    if (!this.currentBlobTx) return;
+
+    try {
+      const receipt = await this.publicClient.getTransactionReceipt({
+        hash: this.currentBlobTx.txHash!
+      });
+
+      if (receipt) {
+        logger.info({
+          txHash: this.currentBlobTx.txHash,
+          blockNumber: receipt.blockNumber
+        }, 'Transaction confirmed');
+
+        // Clear current tx
+        this.currentBlobTx = null;
+      }
+    } catch (error) {
+      // Transaction might still be pending
+    }
+  }
+
+  getPendingTransaction(): PendingTransaction | null {
+    return this.currentBlobTx;
+  }
+
+  private async createNewTx(batch: Batch) {
+    const [baseFee, blobBaseFee] = await Promise.all([
+      this.getBaseFee(),
+      this.getBlobBaseFee()
+    ]);
+
+    const maxFee = baseFee * 2n;
+    const priorityFee = baseFee / 10n; // 10% of base fee as priority
+
+    return {
+      maxFeePerGas: maxFee,
+      maxPriorityFeePerGas: priorityFee < maxFee ? priorityFee : maxFee / 2n,
+      maxFeePerBlobGas: blobBaseFee * 2n
+    };
+  }
+
+  private async createReplacementTx(batch: Batch) {
+    // Smart fee escalation with 12.5% minimum bump
+    const minBump = 1125n; // 12.5% = 1.125 * 1000
+
+    const [currentBaseFee, currentBlobBase] = await Promise.all([
+      this.getBaseFee(),
+      this.getBlobBaseFee()
+    ]);
+
+    // Calculate minimum bumped values (12.5% increase)
+    const minMaxFee = (this.currentBlobTx!.gasPrice! * minBump) / 1000n;
+    const minBlobFee = this.currentBlobTx!.blobGasPrice ?
+      (this.currentBlobTx!.blobGasPrice * minBump) / 1000n :
+      currentBlobBase * 2n;
+
+    // Use higher of: bumped value or 2x current base
+    const maxFee = minMaxFee > (currentBaseFee * 2n) ? minMaxFee : (currentBaseFee * 2n);
+    const blobFee = minBlobFee > (currentBlobBase * 2n) ? minBlobFee : (currentBlobBase * 2n);
+
+    return {
+      maxFeePerGas: maxFee,
+      maxPriorityFeePerGas: maxFee / 10n, // Keep priority at 10%
+      maxFeePerBlobGas: blobFee
+    };
+  }
+
+  private async getBaseFee(): Promise {
+    const block = await this.publicClient.getBlock({ blockTag: 'latest' });
+    return block.baseFeePerGas || 1000000000n; // 1 gwei fallback
+  }
+
+  private async getBlobBaseFee(): Promise {
+    try {
+      return await this.publicClient.getBlobBaseFee();
+    } catch {
+      // Fallback if blob base fee not available
+      return 1000000000n; // 1 gwei
+    }
+  }
+
+  private handleSubmissionError(error: any, batchId: number) {
+    if (error.code === 'INSUFFICIENT_FUNDS') {
+      logger.error('Insufficient funds for blob transaction');
+    } else if (error.code === 'NONCE_TOO_LOW') {
+      // Reset and retry
+      this.currentBlobTx = null;
+      logger.info('Nonce too low, will retry with fresh nonce');
+    }
+    // Don't change batch state on error - keep it sealed for retry
+  }
+}
\ No newline at end of file
diff --git a/sequencer/src/l1/monitor.ts b/sequencer/src/l1/monitor.ts
new file mode 100644
index 00000000..98232311
--- /dev/null
+++ b/sequencer/src/l1/monitor.ts
@@ -0,0 +1,312 @@
+import {
+  createPublicClient,
+  http,
+  type PublicClient,
+  type Hex,
+  type Block,
+  type Transaction,
+  type Chain,
+  keccak256,
+  toRlp
+} from 'viem';
+import type { DatabaseService } from '../db/schema.js';
+import { logger } from '../utils/logger.js';
+
+export class InclusionMonitor {
+  private l1Client: PublicClient;
+  private l2Client: PublicClient;
+  private readonly FACET_MAGIC_PREFIX = '0x756e73746f707061626c652073657175656e63696e67';
+  private isMonitoring = false;
+
+  constructor(
+    private db: DatabaseService,
+    l1RpcUrl: string,
+    l2RpcUrl: string,
+    private l1Chain: Chain,
+    private l2Chain: Chain
+  ) {
+    this.l1Client = createPublicClient({
+      chain: this.l1Chain,
+      transport: http(l1RpcUrl)
+    });
+
+    this.l2Client = createPublicClient({
+      chain: this.l2Chain,
+      transport: http(l2RpcUrl)
+    });
+  }
+  
+  async start(): Promise {
+    if (this.isMonitoring) return;
+    this.isMonitoring = true;
+    
+    logger.info('Starting inclusion monitor');
+    
+    // Monitor L1 blocks for Facet batches
+    const unwatch = this.l1Client.watchBlocks({
+      onBlock: async (block) => {
+        try {
+          await this.scanBlockForFacetBatches(block);
+        } catch (error: any) {
+          logger.error({ error: error.message }, 'Error scanning L1 block');
+        }
+      },
+      pollingInterval: 12000
+    });
+    
+    // Monitor L2 blocks for transaction inclusion
+    const unwatchL2 = this.l2Client.watchBlocks({
+      onBlock: async (block) => {
+        try {
+          await this.checkL2Inclusions(Number(block.number));
+        } catch (error: any) {
+          logger.error({ error: error.message }, 'Error checking L2 inclusions');
+        }
+      },
+      pollingInterval: 1000
+    });
+    
+    // Periodic reorg check
+    setInterval(() => this.checkForReorgs(), 60000);
+  }
+  
+  private async scanBlockForFacetBatches(block: Block): Promise {
+    const blockWithTxs = await this.l1Client.getBlock({
+      blockNumber: block.number!,
+      includeTransactions: true
+    });
+    
+    for (const tx of blockWithTxs.transactions as Transaction[]) {
+      // Check calldata for Facet batches
+      if (tx.input && tx.input.includes(this.FACET_MAGIC_PREFIX)) {
+        await this.handleFacetBatchInCalldata(tx, block);
+      }
+      
+      // Check blob sidecars for Facet batches
+      if (tx.blobVersionedHashes && tx.blobVersionedHashes.length > 0) {
+        await this.checkBlobsForFacetBatch(tx, block);
+      }
+    }
+  }
+  
+  private async checkBlobsForFacetBatch(tx: Transaction, block: Block): Promise {
+    // For each blob hash, try to fetch the blob data
+    for (const blobHash of tx.blobVersionedHashes || []) {
+      try {
+        // In production, this would fetch from beacon API
+        // For now, we'll check our database for matching batches
+        const database = this.db.getDatabase();
+        
+        // Find post attempt for this transaction
+        const attempt = database.prepare(`
+          SELECT * FROM post_attempts 
+          WHERE l1_tx_hash = ? AND status = 'pending'
+        `).get(Buffer.from(tx.hash.slice(2), 'hex')) as any;
+        
+        if (attempt) {
+          await this.handleBatchConfirmation(tx, block, attempt.batch_id);
+        }
+      } catch (error: any) {
+        logger.debug({ error: error.message }, 'Error fetching blob');
+      }
+    }
+  }
+  
+  private async handleFacetBatchInCalldata(tx: Transaction, block: Block): Promise {
+    const database = this.db.getDatabase();
+    
+    // Find post attempt for this transaction
+    const attempt = database.prepare(`
+      SELECT * FROM post_attempts 
+      WHERE l1_tx_hash = ? AND status = 'pending'
+    `).get(Buffer.from(tx.hash.slice(2), 'hex')) as any;
+    
+    if (attempt) {
+      await this.handleBatchConfirmation(tx, block, attempt.batch_id);
+    }
+  }
+  
+  private async handleBatchConfirmation(
+    tx: Transaction, 
+    block: Block, 
+    batchId: number
+  ): Promise {
+    const database = this.db.getDatabase();
+    
+    database.transaction(() => {
+      // Update post attempt
+      database.prepare(`
+        UPDATE post_attempts 
+        SET status = 'mined', 
+            confirmed_at = ?, 
+            block_number = ?,
+            block_hash = ?
+        WHERE l1_tx_hash = ? AND status = 'pending'
+      `).run(
+        Date.now(),
+        Number(block.number),
+        Buffer.from(block.hash!.slice(2), 'hex'),
+        Buffer.from(tx.hash.slice(2), 'hex')
+      );
+      
+      // Update batch state
+      database.prepare(
+        'UPDATE batches SET state = ? WHERE id = ?'
+      ).run('l1_included', batchId);
+      
+      // Update transactions to submitted
+      // Since we now use JSON array in batches.tx_hashes, we just update by batch_id
+      database.prepare(`
+        UPDATE transactions
+        SET state = 'submitted'
+        WHERE batch_id = ? AND state = 'batched'
+      `).run(batchId);
+    })();
+    
+    logger.info({ 
+      batchId, 
+      txHash: tx.hash,
+      blockNumber: block.number 
+    }, 'Batch confirmed on L1');
+  }
+  
+  private async checkL2Inclusions(l2BlockNumber: number): Promise {
+    const database = this.db.getDatabase();
+    
+    try {
+      // Get L2 block with transactions
+      const block = await this.l2Client.getBlock({
+        blockNumber: BigInt(l2BlockNumber),
+        includeTransactions: true
+      });
+      
+      if (!block || !block.transactions) return;
+      
+      const includedHashes = new Set(
+        (block.transactions as Transaction[]).map(tx => tx.hash.toLowerCase())
+      );
+      
+      // Find all submitted transactions
+      const submittedTxs = database.prepare(`
+        SELECT hash FROM transactions 
+        WHERE state = 'submitted'
+      `).all() as Array<{ hash: Buffer }>;
+      
+      for (const tx of submittedTxs) {
+        const txHash = '0x' + tx.hash.toString('hex');
+        if (includedHashes.has(txHash.toLowerCase())) {
+          // Transaction made it into L2!
+          database.prepare(`
+            UPDATE transactions 
+            SET state = 'l2_included', l2_block_number = ?
+            WHERE hash = ?
+          `).run(l2BlockNumber, tx.hash);
+          
+          logger.info({ 
+            txHash, 
+            l2BlockNumber 
+          }, 'Transaction included in L2');
+        }
+      }
+      
+      // Check for dropped transactions
+      this.checkForDroppedTransactions(l2BlockNumber);
+      
+    } catch (error: any) {
+      logger.debug({ error: error.message }, 'Error checking L2 block');
+    }
+  }
+  
+  private checkForDroppedTransactions(l2BlockNumber: number): void {
+    const database = this.db.getDatabase();
+
+    // Transactions submitted more than 10 minutes ago but not included
+    const tenMinutesAgo = Date.now() - (10 * 60 * 1000);
+
+    const dropped = database.prepare(`
+      SELECT DISTINCT t.hash, t.batch_id
+      FROM transactions t
+      JOIN batches b ON t.batch_id = b.id
+      WHERE t.state = 'submitted'
+        AND EXISTS (
+          SELECT 1
+          FROM post_attempts pa
+          WHERE pa.batch_id = b.id
+            AND pa.status = 'mined'
+            AND pa.confirmed_at < ?
+        )
+    `).all(tenMinutesAgo) as Array<{ hash: Buffer; batch_id: number }>;
+
+    for (const tx of dropped) {
+      database.prepare(`
+        UPDATE transactions
+        SET state = 'dropped', drop_reason = 'Not included after 10 minutes'
+        WHERE hash = ?
+      `).run(tx.hash);
+
+      logger.warn({
+        txHash: '0x' + tx.hash.toString('hex')
+      }, 'Transaction dropped');
+    }
+  }
+  
+  private async checkForReorgs(): Promise {
+    const database = this.db.getDatabase();
+    
+    const currentBlock = await this.l1Client.getBlockNumber();
+    
+    const recentAttempts = database.prepare(`
+      SELECT * FROM post_attempts 
+      WHERE status = 'mined' 
+      AND block_number > ? - 10
+    `).all(Number(currentBlock)) as Array<{
+      id: number;
+      batch_id: number;
+      block_number: number;
+      block_hash: Buffer;
+    }>;
+    
+    for (const attempt of recentAttempts) {
+      try {
+        const block = await this.l1Client.getBlock({
+          blockNumber: BigInt(attempt.block_number)
+        });
+        
+        const blockHash = Buffer.from(block.hash!.slice(2), 'hex');
+        if (!block || !blockHash.equals(attempt.block_hash)) {
+          await this.handleReorg(attempt);
+        }
+      } catch (error: any) {
+        logger.error({ error: error.message }, 'Error checking for reorg');
+      }
+    }
+  }
+  
+  private async handleReorg(attempt: any): Promise {
+    const database = this.db.getDatabase();
+    
+    database.transaction(() => {
+      // Mark attempt as reorged
+      database.prepare(
+        'UPDATE post_attempts SET status = ? WHERE id = ?'
+      ).run('reorged', attempt.id);
+      
+      // Revert batch state
+      database.prepare(
+        'UPDATE batches SET state = ? WHERE id = ?'
+      ).run('sealed', attempt.batch_id);
+      
+      // Revert transaction states
+      database.prepare(`
+        UPDATE transactions 
+        SET state = 'batched'
+        WHERE batch_id = ? AND state IN ('submitted', 'l1_included')
+      `).run(attempt.batch_id);
+    })();
+    
+    logger.warn({ 
+      batchId: attempt.batch_id,
+      blockNumber: attempt.block_number 
+    }, 'Reorg detected, reverting batch');
+  }
+}
diff --git a/sequencer/src/l1/poster-interface.ts b/sequencer/src/l1/poster-interface.ts
new file mode 100644
index 00000000..cdc0eaf6
--- /dev/null
+++ b/sequencer/src/l1/poster-interface.ts
@@ -0,0 +1,29 @@
+import type { Hex } from 'viem';
+
+export interface PendingTransaction {
+  batchId: number;
+  txHash?: Hex;
+  requestId?: string;
+  nonce?: number;
+  gasPrice?: bigint;
+  blobGasPrice?: bigint;
+  submittedAt: number;
+  attempts: number;
+}
+
+export interface Poster {
+  /**
+   * Post a batch to L1 (either directly or via DA Builder)
+   */
+  postBatch(batchId: number): Promise;
+
+  /**
+   * Check status of pending transactions and handle confirmations
+   */
+  checkPendingTransaction(): Promise;
+
+  /**
+   * Get current pending transaction info
+   */
+  getPendingTransaction(): PendingTransaction | null;
+}
\ No newline at end of file
diff --git a/sequencer/src/l1/poster.ts b/sequencer/src/l1/poster.ts
new file mode 100644
index 00000000..7f36529f
--- /dev/null
+++ b/sequencer/src/l1/poster.ts
@@ -0,0 +1,339 @@
+import { 
+  createWalletClient, 
+  createPublicClient, 
+  http, 
+  toBlobs,
+  type WalletClient, 
+  type PublicClient,
+  type Hex,
+  type PrivateKeyAccount,
+  type Chain
+} from 'viem';
+import { privateKeyToAccount } from 'viem/accounts';
+import type { DatabaseService, Batch } from '../db/schema.js';
+import { logger } from '../utils/logger.js';
+
+interface PendingBlob {
+  batchId: number;
+  txHash: Hex;
+  nonce: number;
+  gasPrice: bigint;
+  blobGasPrice?: bigint;
+  submittedAt: number;
+  attempts: number;
+}
+
+export class L1Poster {
+  private wallet: WalletClient;
+  private publicClient: PublicClient;
+  private account: PrivateKeyAccount;
+  private currentBlobTx: PendingBlob | null = null;
+  private lastNonce: number = 0;
+  private kzg: any;
+  private kzgReady: Promise;
+  
+  constructor(
+    private db: DatabaseService,
+    private chain: Chain,
+    privateKey: Hex,
+    rpcUrl: string
+  ) {
+    this.account = privateKeyToAccount(privateKey);
+    
+    this.wallet = createWalletClient({
+      account: this.account,
+      chain: this.chain,
+      transport: http(rpcUrl)
+    });
+    
+    this.publicClient = createPublicClient({
+      chain: this.chain,
+      transport: http(rpcUrl)
+    });
+    
+    // Initialize KZG
+    this.kzgReady = this.initKzg();
+  }
+  
+  private async initKzg() {
+    try {
+      const cKzg = await import('c-kzg');
+      // c-kzg 4.x includes the trusted setup internally
+      // Just pass the preset id (0 for mainnet)
+      cKzg.default.loadTrustedSetup(0);
+      this.kzg = cKzg.default;
+      logger.info('KZG initialized successfully');
+    } catch (error: any) {
+      logger.warn({ error: error.message }, 'KZG initialization failed, blob transactions may not work');
+      // Try without any parameters as a fallback
+      try {
+        const cKzg = await import('c-kzg');
+        this.kzg = cKzg.default;
+        logger.info('KZG initialized without explicit trusted setup loading');
+      } catch (e) {
+        logger.error('Failed to initialize KZG completely');
+      }
+    }
+  }
+  
+  async postBatch(batchId: number): Promise {
+    // Wait for KZG to be ready
+    await this.kzgReady;
+    
+    const database = this.db.getDatabase();
+    
+    // Get batch data
+    const batch = database.prepare(
+      'SELECT * FROM batches WHERE id = ? AND state = ?'
+    ).get(batchId, 'sealed') as Batch | undefined;
+    
+    if (!batch) {
+      logger.error({ batchId }, 'Batch not found or not sealed');
+      return;
+    }
+    
+    // Check if previous tx confirmed
+    const currentNonce = await this.publicClient.getTransactionCount({
+      address: this.account.address,
+      blockTag: 'latest'
+    });
+    
+    if (currentNonce > this.lastNonce) {
+      // Previous confirmed, start fresh
+      this.currentBlobTx = null;
+      this.lastNonce = currentNonce;
+    }
+    
+    // Prepare blob transaction
+    const blobTx = this.currentBlobTx ? 
+      await this.createReplacementTx(batch) : 
+      await this.createNewTx(batch);
+    
+    try {
+      // Convert wire format to blobs
+      const wireFormatHex = ('0x' + batch.wire_format.toString('hex')) as Hex;
+      const blobs = toBlobs({ data: wireFormatHex });
+      
+      // Check if KZG is available
+      if (!this.kzg) {
+        throw new Error('KZG not initialized - cannot send blob transaction');
+      }
+      
+      // Submit transaction
+      const txHash = await this.wallet.sendTransaction({
+        account: this.account,
+        chain: this.chain,
+        blobs,
+        kzg: this.kzg, // Pass the KZG instance
+        to: '0x0000000000000000000000000000000000000000' as Hex, // Burn address for blobs
+        nonce: this.currentBlobTx?.nonce || currentNonce,
+        gas: 100000n,
+        maxFeePerGas: blobTx.maxFeePerGas,
+        maxPriorityFeePerGas: blobTx.maxPriorityFeePerGas,
+        maxFeePerBlobGas: blobTx.maxFeePerBlobGas,
+        type: 'eip4844'
+      });
+      
+      // Track for monitoring - make sure we store the actual fees used
+      this.currentBlobTx = {
+        batchId,
+        txHash,
+        nonce: this.currentBlobTx?.nonce || currentNonce,
+        gasPrice: blobTx.maxFeePerGas,  // Store the actual fee we just used
+        blobGasPrice: blobTx.maxFeePerBlobGas,
+        submittedAt: Date.now(),
+        attempts: (this.currentBlobTx?.attempts || 0) + 1
+      };
+      
+      // Store post attempt
+      database.prepare(`
+        INSERT INTO post_attempts (
+          batch_id, l1_tx_hash, l1_nonce, gas_price, 
+          max_fee_per_gas, max_fee_per_blob_gas, submitted_at, status
+        ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending')
+      `).run(
+        batchId,
+        Buffer.from(txHash.slice(2), 'hex'),
+        this.currentBlobTx.nonce,
+        blobTx.maxFeePerGas.toString(),
+        blobTx.maxFeePerGas.toString(),
+        blobTx.maxFeePerBlobGas.toString(),
+        Date.now()
+      );
+      
+      // Update batch state
+      database.prepare(
+        'UPDATE batches SET state = ? WHERE id = ?'
+      ).run('submitted', batchId);
+      
+      logger.info({ 
+        batchId, 
+        txHash,
+        nonce: this.currentBlobTx.nonce,
+        attempt: this.currentBlobTx.attempts 
+      }, 'Batch submitted to L1');
+      
+    } catch (error: any) {
+      logger.error({ batchId, error: error.message }, 'Failed to submit batch');
+      this.handleSubmissionError(error, batchId);
+    }
+  }
+  
+  private async createNewTx(batch: Batch) {
+    const [baseFee, blobBaseFee] = await Promise.all([
+      this.getBaseFee(),
+      this.getBlobBaseFee()
+    ]);
+    
+    const maxFee = baseFee * 2n;
+    const priorityFee = baseFee / 10n; // 10% of base fee as priority
+    
+    return {
+      maxFeePerGas: maxFee,
+      maxPriorityFeePerGas: priorityFee < maxFee ? priorityFee : maxFee / 2n,
+      maxFeePerBlobGas: blobBaseFee * 2n
+    };
+  }
+  
+  private async createReplacementTx(batch: Batch) {
+    // Smart fee escalation with 12.5% minimum bump
+    const oldFee = this.currentBlobTx!.gasPrice;
+    const oldBlobFee = this.currentBlobTx!.blobGasPrice || 1n;
+    
+    const [baseFee, blobBaseFee] = await Promise.all([
+      this.getBaseFee(),
+      this.getBlobBaseFee()
+    ]);
+    
+    // Use BigInt math to avoid precision issues
+    // Always bump by at least 12.5% (multiply by 9/8)
+    const bumpedFee = (oldFee * 9n) / 8n;
+    const bumpedBlobFee = (oldBlobFee * 9n) / 8n;
+    
+    // Also ensure we're at least 2x current base fee
+    const minFee = baseFee * 2n;
+    const minBlobFee = blobBaseFee * 2n;
+    
+    // Take the maximum of bumped fee and minimum required
+    const newFee = bumpedFee > minFee ? bumpedFee : minFee;
+    const newBlobFee = bumpedBlobFee > minBlobFee ? bumpedBlobFee : minBlobFee;
+    
+    // Ensure priority fee is less than max fee (10% of max fee)
+    const priorityFee = newFee / 10n;
+    
+    logger.info({
+      oldFee: oldFee.toString(),
+      newFee: newFee.toString(),
+      baseFee: baseFee.toString(),
+      attempt: this.currentBlobTx!.attempts + 1
+    }, 'Escalating L1 transaction fees');
+    
+    return {
+      maxFeePerGas: newFee,
+      maxPriorityFeePerGas: priorityFee,
+      maxFeePerBlobGas: newBlobFee
+    };
+  }
+  
+  private async getBaseFee(): Promise {
+    const block = await this.publicClient.getBlock({ blockTag: 'latest' });
+    return block.baseFeePerGas || 1000000000n;
+  }
+  
+  private async getBlobBaseFee(): Promise {
+    try {
+      const block = await this.publicClient.getBlock({ blockTag: 'latest' });
+      // @ts-ignore - blobGasPrice might not be in types yet
+      return block.blobGasPrice || 1n;
+    } catch {
+      return 1n;
+    }
+  }
+  
+  private handleSubmissionError(error: any, batchId: number): void {
+    const database = this.db.getDatabase();
+    
+    if (error.message?.includes('replacement transaction underpriced')) {
+      logger.warn({ batchId }, 'RBF underpriced, will retry with higher fee');
+    } else if (error.message?.includes('nonce too low')) {
+      // Reset nonce tracking
+      this.currentBlobTx = null;
+      logger.warn({ batchId }, 'Nonce too low, resetting');
+    } else {
+      // Log failure
+      database.prepare(`
+        UPDATE post_attempts 
+        SET status = 'failed', failure_reason = ?
+        WHERE batch_id = ? AND status = 'pending'
+      `).run(error.message, batchId);
+      
+      // Reset batch to sealed for retry
+      database.prepare(
+        'UPDATE batches SET state = ? WHERE id = ?'
+      ).run('sealed', batchId);
+    }
+  }
+  
+  async checkPendingTransaction(): Promise {
+    if (!this.currentBlobTx) return;
+    
+    try {
+      const receipt = await this.publicClient.getTransactionReceipt({
+        hash: this.currentBlobTx.txHash
+      });
+      
+      if (receipt) {
+        const database = this.db.getDatabase();
+        
+        // Update post attempt
+        database.prepare(`
+          UPDATE post_attempts 
+          SET status = ?, confirmed_at = ?, block_number = ?, block_hash = ?
+          WHERE l1_tx_hash = ? AND status = 'pending'
+        `).run(
+          'mined',
+          Date.now(),
+          Number(receipt.blockNumber),
+          Buffer.from(receipt.blockHash.slice(2), 'hex'),
+          Buffer.from(this.currentBlobTx.txHash.slice(2), 'hex')
+        );
+        
+        // Update batch state
+        database.prepare(
+          'UPDATE batches SET state = ? WHERE id = ?'
+        ).run('l1_included', this.currentBlobTx.batchId);
+        
+        logger.info({ 
+          batchId: this.currentBlobTx.batchId,
+          txHash: receipt.transactionHash,
+          blockNumber: receipt.blockNumber
+        }, 'Batch confirmed on L1');
+        
+        // Clear current transaction
+        this.currentBlobTx = null;
+        this.lastNonce++;
+      } else {
+        // Check if we should escalate fees
+        const timePending = Date.now() - this.currentBlobTx.submittedAt;
+        if (timePending > 30000 && this.currentBlobTx.attempts < 5) {
+          // Escalate after 30 seconds
+          logger.info({ batchId: this.currentBlobTx.batchId }, 'Escalating fees');
+          await this.postBatch(this.currentBlobTx.batchId);
+        }
+      }
+    } catch (error: any) {
+      // Only log actual errors, not "transaction not found" which is expected for pending txs
+      if (!error.message?.includes('could not be found')) {
+        logger.error({ error: error.message }, 'Error checking pending transaction');
+      } else if (this.currentBlobTx) {
+        // Transaction is still pending, this is normal
+        const timePending = Date.now() - this.currentBlobTx.submittedAt;
+        if (timePending > 30000 && this.currentBlobTx.attempts < 5) {
+          // Escalate after 30 seconds
+          logger.info({ batchId: this.currentBlobTx.batchId }, 'Transaction still pending, escalating fees');
+          await this.postBatch(this.currentBlobTx.batchId);
+        }
+      }
+    }
+  }
+}
\ No newline at end of file
diff --git a/sequencer/src/server/api.ts b/sequencer/src/server/api.ts
new file mode 100644
index 00000000..d7629ec4
--- /dev/null
+++ b/sequencer/src/server/api.ts
@@ -0,0 +1,285 @@
+import Fastify, { type FastifyInstance, type FastifyRequest } from 'fastify';
+import cors from '@fastify/cors';
+import { IngressServer } from './ingress.js';
+import type { DatabaseService } from '../db/schema.js';
+import { logger } from '../utils/logger.js';
+import type { Config } from '../config/config.js';
+import type { Hex } from 'viem';
+
+type JsonRpcParams = any[] | Record | undefined;
+
+interface JsonRpcRequest {
+  jsonrpc: string;
+  method: string;
+  params?: JsonRpcParams;
+  id: number | string;
+}
+
+type JsonRpcPayload = JsonRpcRequest | JsonRpcRequest[];
+
+const SPECIAL_METHODS = new Set([
+  'eth_sendRawTransaction',
+  'sequencer_getTxStatus',
+  'sequencer_getStats'
+]);
+
+export class SequencerAPI {
+  private app: FastifyInstance;
+  private ingress: IngressServer;
+  private l2RpcUrl: string;
+
+  constructor(
+    private db: DatabaseService,
+    private config: Config
+  ) {
+    this.app = Fastify({
+      logger: false
+    });
+
+    this.ingress = new IngressServer(db);
+    this.l2RpcUrl = config.l2RpcUrl;
+    this.setupEndpoints();
+  }
+  
+  private setupEndpoints(): void {
+    // Enable CORS
+    this.app.register(cors, {
+      origin: true
+    });
+    
+    // Main JSON-RPC endpoint
+    this.app.post('/', async (req: FastifyRequest<{ Body: JsonRpcPayload }>, reply) => {
+      const payload = req.body;
+      const requests = Array.isArray(payload) ? payload : [payload];
+
+      const canFastProxy = Array.isArray(payload)
+        && requests.length > 0
+        && requests.every((request) => request && typeof request === 'object' && typeof request.method === 'string')
+        && requests.every((request) => !SPECIAL_METHODS.has((request as JsonRpcRequest).method));
+
+      if (canFastProxy) {
+        const proxied = await this.proxyPayload(payload);
+        reply.send(proxied);
+        return;
+      }
+
+      const responses = await Promise.all(requests.map((request) => this.handleRequest(request)));
+
+      reply.send(Array.isArray(payload) ? responses : responses[0]);
+    });
+    
+    // Health check endpoint
+    this.app.get('/health', async (req, reply) => {
+      const health = await this.checkHealth();
+      reply.code(health.healthy ? 200 : 503).send(health);
+    });
+    
+    // Metrics endpoint
+    this.app.get('/metrics', async (req, reply) => {
+      const metrics = await this.getMetrics();
+      reply.type('text/plain').send(metrics);
+    });
+  }
+  
+  async start(): Promise {
+    try {
+      await this.app.listen({ 
+        port: this.config.port,
+        host: '0.0.0.0'
+      });
+      logger.info({ port: this.config.port }, 'API server started');
+    } catch (error) {
+      logger.error(error, 'Failed to start API server');
+      throw error;
+    }
+  }
+  
+  async stop(): Promise {
+    await this.app.close();
+  }
+
+  private async handleRequest(request: any): Promise {
+    const id = request && typeof request === 'object' && 'id' in request ? request.id : null;
+
+    if (!request || typeof request !== 'object' || typeof request.method !== 'string') {
+      return this.makeError(id, -32600, 'Invalid request');
+    }
+
+    const { method } = request as JsonRpcRequest;
+    const params: JsonRpcParams = request.params;
+
+    if (!SPECIAL_METHODS.has(method)) {
+      return this.proxyToL2(method, params, id);
+    }
+
+    try {
+      switch (method) {
+        case 'eth_sendRawTransaction': {
+          const rawTx = Array.isArray(params) ? params[0] : params;
+          if (typeof rawTx !== 'string') {
+            throw new Error('Invalid raw transaction parameter');
+          }
+          const hash = await this.ingress.handleTransaction(rawTx as Hex);
+          return { jsonrpc: '2.0', result: hash, id };
+        }
+        case 'sequencer_getTxStatus': {
+          const txHash = Array.isArray(params) ? params[0] : params;
+          if (typeof txHash !== 'string') {
+            throw new Error('Invalid tx hash parameter');
+          }
+          const status = await this.ingress.getTransactionStatus(txHash as Hex);
+          return { jsonrpc: '2.0', result: status, id };
+        }
+        case 'sequencer_getStats': {
+          const stats = await this.getStats();
+          return { jsonrpc: '2.0', result: stats, id };
+        }
+        default:
+          return this.proxyToL2(method, params, id);
+      }
+    } catch (error: any) {
+      logger.error({ method, error: error.message }, 'RPC error');
+      return this.makeError(id, -32000, error.message || 'Unhandled error');
+    }
+  }
+
+  private async proxyToL2(method: string, params: JsonRpcParams, id: number | string | null): Promise {
+    try {
+      // Forward the exact RPC request to L2
+      const response = await fetch(this.l2RpcUrl, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({
+          jsonrpc: '2.0',
+          method,
+          params: params ?? [],
+          id
+        })
+      });
+
+      const result = await response.json();
+
+      // Log proxied methods for debugging (but not too verbose)
+      if (!['eth_getBlockByNumber', 'eth_blockNumber', 'eth_getBalance'].includes(method)) {
+        logger.debug({ method, proxiedTo: this.l2RpcUrl }, 'Proxied RPC method');
+      }
+
+      return result;
+    } catch (error: any) {
+      logger.error({ method, error: error.message }, 'Proxy to L2 failed');
+      return this.makeError(id, -32000, `Proxy error: ${error.message}`);
+    }
+  }
+
+  private async proxyPayload(payload: JsonRpcPayload): Promise {
+    try {
+      const response = await fetch(this.l2RpcUrl, {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify(payload)
+      });
+
+      return await response.json();
+    } catch (error: any) {
+      logger.error({ error: error.message }, 'Batch proxy to L2 failed');
+      return this.makeBatchProxyError(payload, `Proxy error: ${error.message}`);
+    }
+  }
+
+  private makeBatchProxyError(payload: JsonRpcPayload, message: string) {
+    if (Array.isArray(payload)) {
+      return payload.map((request) => {
+        const id = request && typeof request === 'object' && 'id' in request ? request.id : null;
+        return this.makeError(id, -32000, message);
+      });
+    }
+
+    const id = payload && typeof payload === 'object' && 'id' in payload ? payload.id : null;
+    return this.makeError(id, -32000, message);
+  }
+
+  private makeError(id: number | string | null, code: number, message: string) {
+    return {
+      jsonrpc: '2.0',
+      error: { code, message },
+      id
+    };
+  }
+  
+  private async checkHealth(): Promise {
+    const database = this.db.getDatabase();
+
+    const stats = database.prepare(`
+      SELECT
+        (SELECT COUNT(*) FROM transactions WHERE state IN ('queued', 'requeued')) as queued,
+        (SELECT COUNT(*) FROM batches WHERE state IN ('sealed', 'submitted')) as pending_batches,
+        (SELECT MAX(confirmed_at) FROM post_attempts WHERE status = 'mined') as last_confirmation
+    `).get() as any;
+
+    const now = Date.now();
+    const healthy =
+      stats.queued < this.config.maxPendingTxs &&
+      (!stats.last_confirmation || (now - stats.last_confirmation) < 300000);
+
+    // Check DA Builder health if enabled
+    let daBuilderStatus = undefined;
+    if (this.config.useDABuilder) {
+      daBuilderStatus = {
+        enabled: true,
+        url: this.config.daBuilderUrl,
+        lastSuccessMs: null,  // TODO: implement actual health monitoring
+        healthy: true  // TODO: implement actual health check
+      };
+    }
+
+    return {
+      healthy: healthy && (!daBuilderStatus || daBuilderStatus.healthy),
+      uptime: process.uptime(),
+      queuedTxs: stats.queued,
+      pendingBatches: stats.pending_batches,
+      lastL1Confirmation: stats.last_confirmation,
+      daBuilder: daBuilderStatus
+    };
+  }
+
+  
+  private async getStats(): Promise {
+    const database = this.db.getDatabase();
+    
+    return database.prepare(`
+      SELECT 
+        (SELECT COUNT(*) FROM transactions WHERE state = 'queued') as queued_txs,
+        (SELECT COUNT(*) FROM transactions WHERE state = 'l2_included') as included_txs,
+        (SELECT COUNT(*) FROM transactions WHERE state = 'dropped') as dropped_txs,
+        (SELECT COUNT(*) FROM batches WHERE state = 'l1_included') as confirmed_batches,
+        (SELECT COUNT(*) FROM batches WHERE state = 'sealed') as pending_batches
+    `).get();
+  }
+  
+  private async getMetrics(): Promise {
+    const stats = await this.getStats();
+    
+    // Prometheus format
+    return `
+# HELP sequencer_queued_txs Number of queued transactions
+# TYPE sequencer_queued_txs gauge
+sequencer_queued_txs ${stats.queued_txs}
+
+# HELP sequencer_included_txs_total Total included transactions
+# TYPE sequencer_included_txs_total counter
+sequencer_included_txs_total ${stats.included_txs}
+
+# HELP sequencer_dropped_txs_total Total dropped transactions  
+# TYPE sequencer_dropped_txs_total counter
+sequencer_dropped_txs_total ${stats.dropped_txs}
+
+# HELP sequencer_confirmed_batches_total Total confirmed batches
+# TYPE sequencer_confirmed_batches_total counter
+sequencer_confirmed_batches_total ${stats.confirmed_batches}
+
+# HELP sequencer_pending_batches Number of pending batches
+# TYPE sequencer_pending_batches gauge
+sequencer_pending_batches ${stats.pending_batches}
+`.trim();
+  }
+}
diff --git a/sequencer/src/server/ingress.ts b/sequencer/src/server/ingress.ts
new file mode 100644
index 00000000..f80c9a5a
--- /dev/null
+++ b/sequencer/src/server/ingress.ts
@@ -0,0 +1,278 @@
+import {
+  parseTransaction,
+  type TransactionSerializableEIP1559,
+  type TransactionSerializableEIP2930,
+  type TransactionSerializableLegacy,
+  type TransactionSerializedEIP1559,
+  type TransactionSerializedEIP2930,
+  type TransactionSerializedLegacy,
+  keccak256,
+  type Hex,
+  toHex,
+  recoverTransactionAddress
+} from 'viem';
+import type { DatabaseService } from '../db/schema.js';
+import { logger } from '../utils/logger.js';
+
+export class IngressServer {
+  private readonly MAX_PENDING = 10000;
+  private readonly MIN_BASE_FEE = 1000000n; // 1 gwei
+  private readonly MAX_TX_SIZE = 128 * 1024; // 128KB
+  private readonly BLOCK_GAS_LIMIT = 100_000_000;
+  
+  constructor(private db: DatabaseService) {}
+  
+  async handleTransaction(rawTx: Hex): Promise {
+    // Input sanitization
+    if (!rawTx.startsWith('0x') || rawTx.length % 2 !== 0) {
+      throw new Error('Invalid hex encoding');
+    }
+    
+    if (rawTx.length > this.MAX_TX_SIZE * 2) {
+      throw new Error('Transaction too large');
+    }
+    
+    // Back-pressure check
+    const queuedCount = this.db.getDatabase().prepare(
+      'SELECT COUNT(*) as count FROM transactions WHERE state IN (?, ?)'
+    ).get('queued', 'requeued') as { count: number };
+    
+    if (queuedCount.count >= this.MAX_PENDING) {
+      throw new Error('Sequencer busy');
+    }
+    
+    // Decode and validate transaction
+    let tx: TransactionSerializableEIP1559 | TransactionSerializableEIP2930 | TransactionSerializableLegacy;
+    let from: Hex;
+    let maxFeePerGas: bigint;
+    let maxPriorityFeePerGas: bigint | undefined;
+
+    try {
+      const parsed = parseTransaction(rawTx);
+
+      // Accept legacy (type 0), EIP-2930 (type 1), and EIP-1559 (type 2)
+      // Reject type 3 (blob transactions) and beyond
+      if (parsed.type !== 'eip1559' && parsed.type !== 'eip2930' && parsed.type !== 'legacy') {
+        throw new Error('Only legacy, EIP-2930, and EIP-1559 transactions accepted');
+      }
+
+      tx = parsed;
+
+      // Extract fee values based on transaction type and recover address
+      if (parsed.type === 'legacy' || parsed.type === 'eip2930') {
+        // Legacy and EIP-2930 both use gasPrice
+        const gasPriceTx = tx as TransactionSerializableLegacy | TransactionSerializableEIP2930;
+
+        if (!gasPriceTx.gasPrice || gasPriceTx.gasPrice < this.MIN_BASE_FEE) {
+          throw new Error('Gas price below minimum');
+        }
+        maxFeePerGas = gasPriceTx.gasPrice;
+        maxPriorityFeePerGas = undefined;  // NULL for legacy/EIP-2930
+
+        // Recover from address with proper type
+        from = await recoverTransactionAddress({
+          serializedTransaction: rawTx as TransactionSerializedLegacy | TransactionSerializedEIP2930
+        });
+      } else {
+        // EIP-1559
+        const eip1559Tx = tx as TransactionSerializableEIP1559;
+
+        if (!eip1559Tx.maxFeePerGas || eip1559Tx.maxFeePerGas < this.MIN_BASE_FEE) {
+          throw new Error('Max fee per gas below minimum');
+        }
+
+        if (!eip1559Tx.maxPriorityFeePerGas) {
+          throw new Error('Priority fee required');
+        }
+
+        maxFeePerGas = eip1559Tx.maxFeePerGas;
+        maxPriorityFeePerGas = eip1559Tx.maxPriorityFeePerGas;
+
+        // Recover from address with proper type
+        from = await recoverTransactionAddress({
+          serializedTransaction: rawTx as TransactionSerializedEIP1559
+        });
+      }
+
+      if (!from) {
+        throw new Error('Could not recover sender address');
+      }
+    } catch (e: any) {
+      throw new Error('Invalid transaction encoding: ' + e.message);
+    }
+
+    if (!tx.gas || tx.gas > BigInt(this.BLOCK_GAS_LIMIT)) {
+      throw new Error('Invalid gas limit');
+    }
+    
+    // Calculate intrinsic gas
+    const intrinsicGas = this.calculateIntrinsicGas(tx);
+    if (intrinsicGas > Number(tx.gas)) {
+      throw new Error('Gas limit below intrinsic gas');
+    }
+    
+    // Calculate transaction hash
+    const txHash = keccak256(rawTx);
+    
+    // Store transaction atomically
+    const database = this.db.getDatabase();
+    const result = database.transaction(() => {
+      // Get next sequence number
+      const seqResult = database.prepare(
+        'SELECT COALESCE(MAX(received_seq), 0) + 1 as next_seq FROM transactions'
+      ).get() as { next_seq: number };
+      
+      // Check for duplicate hash
+      const existing = database.prepare(
+        'SELECT hash FROM transactions WHERE hash = ?'
+      ).get(Buffer.from(txHash.slice(2), 'hex'));
+      
+      if (existing) {
+        return { exists: true, hash: txHash, replaced: false };
+      }
+      
+      // Check for same nonce (potential replacement)
+      const sameNonce = database.prepare(
+        'SELECT hash, max_fee_per_gas FROM transactions WHERE from_address = ? AND nonce = ? AND state = ?'
+      ).get(
+        Buffer.from(from.slice(2), 'hex'),
+        Number(tx.nonce || 0),
+        'queued'
+      ) as any;
+      
+      if (sameNonce) {
+        // Replace-by-fee: new transaction must have higher gas price
+        const oldMaxFee = BigInt(sameNonce.max_fee_per_gas);
+        const newMaxFee = maxFeePerGas;
+
+        if (newMaxFee > oldMaxFee) {
+          // Delete old transaction and insert new one
+          database.prepare('DELETE FROM transactions WHERE hash = ?').run(sameNonce.hash);
+          logger.info({
+            oldHash: '0x' + sameNonce.hash.toString('hex'),
+            newHash: txHash,
+            oldFee: oldMaxFee.toString(),
+            newFee: newMaxFee.toString()
+          }, 'Replacing transaction with higher gas price');
+        } else {
+          throw new Error('Replacement transaction underpriced');
+        }
+      }
+      
+      // Insert transaction
+      const stmt = database.prepare(`
+        INSERT INTO transactions (
+          hash, raw, from_address, nonce, max_fee_per_gas,
+          max_priority_fee_per_gas, gas_limit, intrinsic_gas,
+          received_seq, received_at, state
+        ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+      `);
+
+      stmt.run(
+        Buffer.from(txHash.slice(2), 'hex'),
+        Buffer.from(rawTx.slice(2), 'hex'),
+        Buffer.from(from.slice(2), 'hex'),
+        Number(tx.nonce || 0),
+        maxFeePerGas.toString(),
+        maxPriorityFeePerGas?.toString() || null,  // NULL for legacy
+        Number(tx.gas),
+        intrinsicGas,
+        seqResult.next_seq,
+        Date.now(),
+        'queued'
+      );
+      
+      return { exists: false, hash: txHash, replaced: !!sameNonce };
+    })();
+    
+    if (!result.exists) {
+      logger.info({ hash: txHash }, 'Transaction accepted');
+    }
+    
+    return txHash;
+  }
+  
+  private calculateIntrinsicGas(tx: TransactionSerializableEIP1559 | TransactionSerializableEIP2930 | TransactionSerializableLegacy): number {
+    // Base cost
+    let gas = 21000;
+    
+    // Contract creation cost
+    if (!tx.to) {
+      gas += 32000;
+    }
+    
+    // Data cost (4 gas per zero byte, 16 per non-zero)
+    if (tx.data) {
+      const data = typeof tx.data === 'string' ? tx.data : toHex(tx.data);
+      const bytes = Buffer.from(data.slice(2), 'hex');
+      for (const byte of bytes) {
+        gas += byte === 0 ? 4 : 16;
+      }
+    }
+    
+    // Access list cost (only for EIP-1559 and EIP-2930)
+    if ('accessList' in tx && tx.accessList && tx.accessList.length > 0) {
+      for (const entry of tx.accessList) {
+        gas += 2400; // Address cost
+        gas += 1900 * (entry.storageKeys?.length || 0); // Storage key cost
+      }
+    }
+    
+    return gas;
+  }
+  
+  async getTransactionStatus(hash: Hex): Promise {
+    // First query: get transaction and batch info
+    const tx = this.db.getDatabase().prepare(`
+      SELECT
+        t.state,
+        t.batch_id,
+        t.l2_block_number,
+        t.drop_reason,
+        b.state as batch_state
+      FROM transactions t
+      LEFT JOIN batches b ON t.batch_id = b.id
+      WHERE t.hash = ?
+    `).get(Buffer.from(hash.slice(2), 'hex')) as any;
+
+    if (!tx) {
+      return { status: 'unknown' };
+    }
+
+    // Second query: get the best post_attempt if batch exists
+    let postAttempt: any = null;
+    if (tx.batch_id) {
+      postAttempt = this.db.getDatabase().prepare(`
+        SELECT
+          l1_tx_hash,
+          da_builder_request_id,
+          block_number as l1_block,
+          status as attempt_status
+        FROM post_attempts
+        WHERE batch_id = ?
+        AND status IN ('mined', 'pending')
+        ORDER BY
+          CASE status WHEN 'mined' THEN 2 ELSE 1 END DESC,
+          COALESCE(confirmed_at, submitted_at, 0) DESC,
+          id DESC
+        LIMIT 1
+      `).get(tx.batch_id);
+    }
+
+    // Derive submission mode from presence of da_builder_request_id
+    const submissionMode = postAttempt?.da_builder_request_id ? 'da_builder' :
+                          postAttempt ? 'direct' : undefined;
+
+    return {
+      status: tx.state,
+      batchId: tx.batch_id,
+      batchState: tx.batch_state,
+      submissionMode,
+      l1TxHash: postAttempt?.l1_tx_hash ? '0x' + postAttempt.l1_tx_hash.toString('hex') : undefined,
+      daRequestId: postAttempt?.da_builder_request_id || undefined,
+      l1Block: postAttempt?.l1_block,
+      l2Block: tx.l2_block_number,
+      dropReason: tx.drop_reason
+    };
+  }
+}
\ No newline at end of file
diff --git a/sequencer/src/utils/logger.ts b/sequencer/src/utils/logger.ts
new file mode 100644
index 00000000..065ab4fa
--- /dev/null
+++ b/sequencer/src/utils/logger.ts
@@ -0,0 +1,13 @@
+import pino from 'pino';
+
+export const logger = pino({
+  level: process.env.LOG_LEVEL || 'info',
+  transport: process.env.NODE_ENV !== 'production' ? {
+    target: 'pino-pretty',
+    options: {
+      colorize: true,
+      translateTime: 'HH:MM:ss Z',
+      ignore: 'pid,hostname'
+    }
+  } : undefined
+});
\ No newline at end of file
diff --git a/sequencer/src/utils/tx-cache.ts b/sequencer/src/utils/tx-cache.ts
new file mode 100644
index 00000000..92311229
--- /dev/null
+++ b/sequencer/src/utils/tx-cache.ts
@@ -0,0 +1,108 @@
+/**
+ * Simple in-memory transaction status cache for RPC responses
+ * This replaces complex database state tracking
+ */
+
+import { type Hex } from 'viem';
+
+interface TxStatus {
+  hash: Hex;
+  status: 'pending' | 'batched' | 'included' | 'failed';
+  timestamp: number;
+  batchId?: number;
+  l2BlockNumber?: number;
+  l2BlockHash?: Hex;
+  receipt?: any; // Full receipt once included
+}
+
+export class TxStatusCache {
+  private cache = new Map();
+  private readonly TTL_MS = 60 * 60 * 1000; // 1 hour
+
+  constructor() {
+    // Cleanup old entries every 5 minutes
+    setInterval(() => this.cleanup(), 5 * 60 * 1000);
+  }
+
+  setPending(hash: Hex): void {
+    this.cache.set(hash.toLowerCase(), {
+      hash,
+      status: 'pending',
+      timestamp: Date.now()
+    });
+  }
+
+  setBatched(hash: Hex, batchId: number): void {
+    const existing = this.cache.get(hash.toLowerCase());
+    if (existing) {
+      existing.status = 'batched';
+      existing.batchId = batchId;
+    }
+  }
+
+  setIncluded(hash: Hex, blockNumber: number, blockHash: Hex, receipt: any): void {
+    const existing = this.cache.get(hash.toLowerCase());
+    if (existing) {
+      existing.status = 'included';
+      existing.l2BlockNumber = blockNumber;
+      existing.l2BlockHash = blockHash;
+      existing.receipt = receipt;
+    } else {
+      // Even if we didn't track it before, cache the result
+      this.cache.set(hash.toLowerCase(), {
+        hash,
+        status: 'included',
+        timestamp: Date.now(),
+        l2BlockNumber: blockNumber,
+        l2BlockHash: blockHash,
+        receipt
+      });
+    }
+  }
+
+  get(hash: Hex): TxStatus | undefined {
+    return this.cache.get(hash.toLowerCase());
+  }
+
+  getReceipt(hash: Hex): any | null {
+    const status = this.cache.get(hash.toLowerCase());
+    if (status?.status === 'included' && status.receipt) {
+      return status.receipt;
+    }
+    return null;
+  }
+
+  private cleanup(): void {
+    const now = Date.now();
+    const expired: string[] = [];
+
+    for (const [hash, status] of this.cache.entries()) {
+      if (now - status.timestamp > this.TTL_MS) {
+        expired.push(hash);
+      }
+    }
+
+    for (const hash of expired) {
+      this.cache.delete(hash);
+    }
+
+    if (expired.length > 0) {
+      console.log(`Cleaned up ${expired.length} expired tx statuses`);
+    }
+  }
+
+  // For monitoring
+  stats(): { pending: number; batched: number; included: number; total: number } {
+    let pending = 0, batched = 0, included = 0;
+
+    for (const status of this.cache.values()) {
+      switch (status.status) {
+        case 'pending': pending++; break;
+        case 'batched': batched++; break;
+        case 'included': included++; break;
+      }
+    }
+
+    return { pending, batched, included, total: this.cache.size };
+  }
+}
\ No newline at end of file
diff --git a/sequencer/tsconfig.json b/sequencer/tsconfig.json
new file mode 100644
index 00000000..f82674a0
--- /dev/null
+++ b/sequencer/tsconfig.json
@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "target": "ES2022",
+    "module": "ESNext",
+    "moduleResolution": "node",
+    "lib": ["ES2022"],
+    "outDir": "./dist",
+    "rootDir": "./src",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "resolveJsonModule": true,
+    "declaration": true,
+    "declarationMap": true,
+    "sourceMap": true,
+    "noUnusedLocals": false,
+    "noUnusedParameters": false,
+    "noImplicitReturns": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedIndexedAccess": false,
+    "noImplicitAny": true,
+    "strictNullChecks": true,
+    "types": ["node"]
+  },
+  "include": ["src/**/*"],
+  "exclude": ["node_modules", "dist", "test"]
+}
\ No newline at end of file
diff --git a/spec/integration/blob_end_to_end_spec.rb b/spec/integration/blob_end_to_end_spec.rb
new file mode 100644
index 00000000..ddb25c51
--- /dev/null
+++ b/spec/integration/blob_end_to_end_spec.rb
@@ -0,0 +1,250 @@
+require 'rails_helper'
+require 'support/blob_test_helper'
+
+RSpec.describe 'Blob End-to-End Integration' do
+  include BlobTestHelper
+  include GethTestHelper
+  
+  let(:collector) { FacetBatchCollector.new }
+  let(:parser) { FacetBatchParser.new }
+  let(:builder) { FacetBlockBuilder.new }
+  
+  describe 'Full blob processing pipeline' do
+    it 'creates, encodes, and parses a blob with Facet batch data' do
+      # Step 1: Create test transactions
+      puts "\n=== Creating test transactions ==="
+      transactions = [
+        create_test_transaction(to: "0x" + "1" * 40, value: 1000, nonce: 0),
+        create_test_transaction(to: "0x" + "2" * 40, value: 2000, nonce: 1),
+        create_test_transaction(to: "0x" + "3" * 40, value: 3000, nonce: 2)
+      ]
+      
+      transactions.each_with_index do |tx, i|
+        puts "  Transaction #{i}: to=#{tx.to_hex[0..10]}... value=#{i * 1000 + 1000}"
+      end
+      
+      # Step 2: Create a Facet batch
+      puts "\n=== Creating Facet batch ==="
+      rlp_tx_list = create_test_batch_data(transactions)
+      puts "  Transaction list size: #{rlp_tx_list.bytesize} bytes"
+      puts "  Batch contains #{transactions.length} transactions"
+
+      # Step 3: Create blob with Facet data (simulating DA Builder aggregation)
+      puts "\n=== Encoding to EIP-4844 blob ==="
+
+      # Build complete wire format
+      chain_id = ChainIdManager.current_l2_chain_id
+      facet_payload = FacetBatchConstants::MAGIC_PREFIX.to_bin
+      facet_payload += [chain_id].pack('Q>')  # uint64 big-endian
+      facet_payload += [FacetBatchConstants::VERSION].pack('C')
+      facet_payload += [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+      facet_payload += [rlp_tx_list.length].pack('N')
+      facet_payload += rlp_tx_list
+      
+      # Simulate aggregation with other data
+      other_rollup_data = "\xDE\xAD\xBE\xEF".b * 1000  # 4KB of other data
+      aggregated = other_rollup_data + facet_payload + ("\xCA\xFE".b * 500)
+      
+      puts "  Total aggregated data: #{aggregated.bytesize} bytes"
+      puts "  Facet data starts at offset: #{other_rollup_data.bytesize}"
+      
+      # Encode to blob
+      blobs = BlobUtils.to_blobs(data: aggregated)
+      puts "  Created #{blobs.length} blob(s)"
+      puts "  Blob size: #{blobs.first.length / 2 - 1} bytes (hex: #{blobs.first.length} chars)"
+      
+      # Step 4: Simulate beacon provider returning the blob
+      puts "\n=== Simulating beacon provider ==="
+      versioned_hash = "0x01" + ("a" * 62)
+      blob_bytes = ByteString.from_hex(blobs.first)
+      
+      beacon_provider = stub_beacon_blob_response(versioned_hash, blob_bytes)
+      puts "  Stubbed beacon provider with versioned hash: #{versioned_hash[0..10]}..."
+      
+      # Step 5: Fetch and decode the blob
+      puts "\n=== Fetching and decoding blob ==="
+      fetched_blob = beacon_provider.get_blob(versioned_hash, block_number: 12345)
+      expect(fetched_blob).not_to be_nil
+      puts "  Successfully fetched blob"
+      
+      # Step 6: Parse Facet batches from the decoded blob
+      puts "\n=== Parsing Facet batches from blob ==="
+      
+      # Decode from EIP-4844 format
+      decoded_data = BlobUtils.from_blobs(blobs: [fetched_blob.to_hex])
+      decoded_bytes = ByteString.from_hex(decoded_data)
+      puts "  Decoded data size: #{decoded_bytes.to_bin.bytesize} bytes"
+      
+      # Parse batches
+      parsed_batches = parser.parse_payload(
+        decoded_bytes,
+        0,      # l1_tx_index
+        FacetBatchConstants::Source::BLOB,
+        { versioned_hash: versioned_hash }
+      )
+      
+      puts "  Found #{parsed_batches.length} Facet batch(es)"
+      
+      # Step 7: Verify the parsed batch
+      puts "\n=== Verifying parsed batch ==="
+      expect(parsed_batches.length).to eq(1)
+      
+      batch = parsed_batches.first
+      expect(batch.transactions.length).to eq(3)
+      expect(batch.source).to eq(FacetBatchConstants::Source::BLOB)
+      expect(batch.role).to eq(FacetBatchConstants::Role::PERMISSIONLESS)
+      
+      puts "  ✓ Batch role: #{batch.role == FacetBatchConstants::Role::PRIORITY ? 'PRIORITY' : 'PERMISSIONLESS'}"
+      puts "  ✓ Transaction count: #{batch.transactions.length}"
+      puts "  ✓ Source: #{batch.source_description}"
+      puts "  ✓ Chain ID: #{batch.chain_id}"
+      
+      # Verify transaction details
+      batch.transactions.each_with_index do |tx, i|
+        expected_value = (i + 1) * 1000
+        actual_value = Eth::Rlp.decode(tx.to_bin[1..-1])[6]
+        actual_value = actual_value.empty? ? 0 : Eth::Util.deserialize_big_endian_to_int(actual_value)
+        
+        puts "  ✓ Transaction #{i}: value=#{actual_value} (expected #{expected_value})"
+        expect(actual_value).to eq(expected_value)
+      end
+      
+      puts "\n=== ✅ All tests passed! ==="
+    end
+    
+    it 'handles multiple Facet batches in a single blob' do
+      puts "\n=== Testing multiple batches in one blob ==="
+      
+      # Create two separate batches
+      batch1_txs = [create_test_transaction(value: 100, nonce: 0)]
+      batch2_txs = [create_test_transaction(value: 200, nonce: 1)]
+
+      # Create RLP transaction lists
+      rlp_tx_list1 = create_test_batch_data(batch1_txs)
+      rlp_tx_list2 = create_test_batch_data(batch2_txs)
+
+      # Build complete wire format for each batch
+      chain_id = ChainIdManager.current_l2_chain_id
+
+      # First batch
+      payload1 = FacetBatchConstants::MAGIC_PREFIX.to_bin
+      payload1 += [chain_id].pack('Q>')  # uint64 big-endian
+      payload1 += [FacetBatchConstants::VERSION].pack('C')
+      payload1 += [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+      payload1 += [rlp_tx_list1.length].pack('N')
+      payload1 += rlp_tx_list1
+
+      # Second batch
+      payload2 = FacetBatchConstants::MAGIC_PREFIX.to_bin
+      payload2 += [chain_id].pack('Q>')  # uint64 big-endian
+      payload2 += [FacetBatchConstants::VERSION].pack('C')
+      payload2 += [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+      payload2 += [rlp_tx_list2.length].pack('N')
+      payload2 += rlp_tx_list2
+      
+      # Aggregate with padding
+      aggregated = payload1 + ("\x00".b * 1000) + payload2
+      
+      # Encode to blob
+      blobs = BlobUtils.to_blobs(data: aggregated)
+      
+      # Decode and parse
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      decoded_bytes = ByteString.from_hex(decoded)
+      
+      parsed_batches = parser.parse_payload(
+        decoded_bytes,
+        0,
+        FacetBatchConstants::Source::BLOB
+      )
+      
+      expect(parsed_batches.length).to eq(2)
+      puts "  ✓ Found #{parsed_batches.length} batches"
+      
+      expect(parsed_batches[0].transactions.length).to eq(1)
+      expect(parsed_batches[1].transactions.length).to eq(1)
+      puts "  ✓ Each batch has correct transaction count"
+    end
+    
+    it 'correctly handles blob size limits' do
+      puts "\n=== Testing blob size limits ==="
+      
+      # Create maximum size data (just under limit)
+      max_size = BlobUtils::MAX_BYTES_PER_TRANSACTION - 1000
+      large_data = "A" * max_size
+      
+      # Should succeed
+      blobs = BlobUtils.to_blobs(data: large_data)
+      expect(blobs.length).to be >= 1
+      puts "  ✓ Successfully encoded #{max_size} bytes into #{blobs.length} blob(s)"
+      
+      # Test oversized data
+      oversized = "B" * (BlobUtils::MAX_BYTES_PER_TRANSACTION + 1)
+      
+      expect {
+        BlobUtils.to_blobs(data: oversized)
+      }.to raise_error(BlobUtils::BlobSizeTooLargeError)
+      puts "  ✓ Correctly rejected oversized data"
+    end
+    
+    it 'preserves data integrity through encode/decode cycle' do
+      puts "\n=== Testing data integrity ==="
+      
+      # Test various data patterns
+      test_cases = [
+        { name: "Binary data", data: "\x00\x01\x02\x80\xFF".b * 100 },
+        { name: "Text data", data: "Hello, Facet! " * 1000 },
+        { name: "Hex string", data: "0x" + ("deadbeefcafe" * 100) },
+        { name: "Mixed content", data: "Text\x00Binary\x80\xFFMore".b }
+      ]
+      
+      test_cases.each do |test_case|
+        puts "\n  Testing: #{test_case[:name]}"
+        
+        # Encode
+        blobs = BlobUtils.to_blobs(data: test_case[:data])
+        puts "    Encoded to #{blobs.length} blob(s)"
+        
+        # Decode
+        decoded = BlobUtils.from_blobs(blobs: blobs)
+        
+        # Compare (accounting for hex conversion)
+        if test_case[:data].start_with?("0x")
+          expect(decoded).to eq(test_case[:data])
+        else
+          expect(decoded).to eq("0x" + test_case[:data].unpack1('H*'))
+        end
+        
+        puts "    ✓ Data integrity preserved"
+      end
+    end
+  end
+  
+  describe 'Error handling' do
+    it 'handles corrupted magic prefix gracefully' do
+      bad_magic = "\x00\x00\x00\x00\x00\x01\x23\x46".b  # Wrong last byte
+      payload = bad_magic + [100].pack('N') + ("X".b * 100)
+      
+      blobs = BlobUtils.to_blobs(data: payload)
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      decoded_bytes = ByteString.from_hex(decoded)
+      
+      batches = parser.parse_payload(decoded_bytes, 0, FacetBatchConstants::Source::BLOB)
+      
+      expect(batches).to be_empty
+      puts "  ✓ Correctly ignored batch with bad magic"
+    end
+    
+    it 'handles empty blobs' do
+      expect {
+        BlobUtils.to_blobs(data: "")
+      }.to raise_error(BlobUtils::EmptyBlobError)
+      
+      expect {
+        BlobUtils.to_blobs(data: "0x")
+      }.to raise_error(BlobUtils::EmptyBlobError)
+      
+      puts "  ✓ Correctly rejected empty blobs"
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/integration/forced_tx_filtering_spec.rb b/spec/integration/forced_tx_filtering_spec.rb
new file mode 100644
index 00000000..5bd3a3d2
--- /dev/null
+++ b/spec/integration/forced_tx_filtering_spec.rb
@@ -0,0 +1,129 @@
+require 'rails_helper'
+
+RSpec.describe 'Forced Transaction Filtering' do
+  include FacetTransactionHelper
+
+  before do
+    allow(SysConfig).to receive(:facet_batch_v2_enabled?).and_return(true)
+    # Silence blob fetching in tests to avoid noisy warnings
+    allow_any_instance_of(BlobProvider).to receive(:list_carriers).and_return([])
+    allow_any_instance_of(BlobProvider).to receive(:get_blob).and_return(nil)
+  end
+
+  it 'filters invalid forced txs (pre-flight) and still builds the block' do
+    importer = ImporterSingleton.instance
+    current_max_eth_block = importer.current_max_eth_block
+
+    # Use a deterministic key for a funded account
+    funded_priv = '0x0000000000000000000000000000000000000000000000000000000000000003'
+    funded_addr = Eth::Key.new(priv: funded_priv).address.to_s
+
+    # 1) Fund the account with a Facet V1 single (calldata-based mint)
+    funding_data = '0x' + 'ff' * 5000
+    funding_payload = generate_facet_tx_payload(
+      input: funding_data,
+      to: '0x' + 'a' * 40,
+      gas_limit: 10_000_000,
+      value: 0
+    )
+
+    funding_receipts = import_eth_txs([
+      {
+        input: funding_payload,
+        from_address: funded_addr,
+        to_address: EthTransaction::FACET_INBOX_ADDRESS.to_hex
+      }
+    ])
+    expect(funding_receipts.first).to be_present
+    expect(funding_receipts.first.status).to eq(1)
+
+    # 2) Build one valid EIP-1559 tx from the funded account
+    valid_tx = create_eip1559_transaction(
+      private_key: funded_priv,
+      to: '0x' + 'c' * 40,
+      value: 0,
+      gas_limit: 21_000
+    )
+
+    # 3) Build one invalid EIP-1559 tx (insufficient funds) from a fresh account
+    unfunded_priv = '0x00000000000000000000000000000000000000000000000000000000000000aa'
+    unfunded_addr = Eth::Key.new(priv: unfunded_priv).address.to_s
+    invalid_tx = create_eip1559_transaction(
+      private_key: unfunded_priv,
+      to: '0x' + 'd' * 40,
+      value: 0,
+      gas_limit: 21_000
+    )
+
+    # 4) Create a FORCED batch with both transactions (invalid first exercises filtering)
+    target_block = current_max_eth_block.number + 2
+    batch_payload = create_forced_batch_payload(
+      transactions: [invalid_tx, valid_tx],
+      target_l1_block: target_block
+    )
+
+    # 5) Import the L1 block that carries the forced batch
+    import_eth_txs([
+      {
+        input: batch_payload.to_hex,
+        from_address: '0x' + '1' * 40,
+        to_address: '0x' + '2' * 40
+      }
+    ])
+
+    # 6) Verify L2 block includes only the valid EIP-1559 tx (plus system tx)
+    latest_l2_block = EthRpcClient.l2.get_block('latest', true)
+    txs = latest_l2_block['transactions']
+
+    # Count EIP-1559 transactions
+    eip1559_count = txs.count { |t| t['type'].to_i(16) == 0x02 }
+    expect(eip1559_count).to eq(1)
+
+    # Ensure the unfunded sender is not present, and the funded sender is present
+    froms = txs.map { |t| (t['from'] || '').downcase }
+    expect(froms).to include(funded_addr.downcase)
+    expect(froms).not_to include(unfunded_addr.downcase)
+  end
+
+  # Helpers (scoped to this spec)
+  def create_eip1559_transaction(private_key:, to:, value:, gas_limit:, nonce: nil)
+    chain_id = ChainIdManager.current_l2_chain_id
+    key = Eth::Key.new(priv: private_key)
+
+    if nonce.nil?
+      nonce = EthRpcClient.l2.call('eth_getTransactionCount', [key.address.to_s, 'latest']).to_i(16)
+    end
+
+    tx = Eth::Tx::Eip1559.new(
+      chain_id: chain_id,
+      nonce: nonce,
+      priority_fee: 1 * Eth::Unit::GWEI,
+      max_gas_fee: 2 * Eth::Unit::GWEI,
+      gas_limit: gas_limit,
+      to: to,
+      value: value,
+      data: ''
+    )
+    tx.sign(key)
+    hex = tx.hex
+    hex = '0x' + hex unless hex.start_with?('0x')
+    ByteString.from_hex(hex)
+  end
+
+  def create_forced_batch_payload(transactions:, target_l1_block:)
+    chain_id = ChainIdManager.current_l2_chain_id
+
+    # Create RLP-encoded transaction list
+    rlp_tx_list = Eth::Rlp.encode(transactions.map(&:to_bin))
+
+    # Build wire format: [MAGIC:#{FacetBatchConstants::MAGIC_SIZE}][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST]
+    payload = FacetBatchConstants::MAGIC_PREFIX.to_bin
+    payload += [chain_id].pack('Q>')  # uint64 big-endian
+    payload += [FacetBatchConstants::VERSION].pack('C')
+    payload += [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+    payload += [rlp_tx_list.length].pack('N')
+    payload += rlp_tx_list
+
+    ByteString.from_bin(payload)
+  end
+end
diff --git a/spec/integration/reorg_duplicate_timestamp_spec.rb b/spec/integration/reorg_duplicate_timestamp_spec.rb
index be644639..75f2bd1f 100644
--- a/spec/integration/reorg_duplicate_timestamp_spec.rb
+++ b/spec/integration/reorg_duplicate_timestamp_spec.rb
@@ -102,6 +102,7 @@ def build_importer(latest_l2:, prefetch_store:)
 
     hash0 = latest_l2['hash']
     zero_root = '0x' + '00' * 32
+    
     eth_genesis = EthBlock.from_rpc_result(
       build_block(number: 0, hash: hash0, parent_hash: '0x' + '00' * 32, timestamp: base_ts, parent_beacon_root: zero_root)
     )
@@ -113,6 +114,7 @@ def build_importer(latest_l2:, prefetch_store:)
     importer.ethereum_client = double('EthRpcClient')
     importer.geth_driver = GethDriver
     importer.prefetcher = build_prefetcher(prefetch_store)
+    
     allow(importer).to receive(:current_block_number).and_return(2)
 
     importer
diff --git a/spec/l1_rpc_prefetcher_spec.rb b/spec/l1_rpc_prefetcher_spec.rb
index eae0b91a..5035d3cb 100644
--- a/spec/l1_rpc_prefetcher_spec.rb
+++ b/spec/l1_rpc_prefetcher_spec.rb
@@ -13,12 +13,13 @@
   end
   
   describe '#fetch' do
-    let(:block_data) { { 'number' => '0x1', 'hash' => '0x123' } }
+    let(:block_data) { { 'number' => '0x1', 'hash' => '0x123', "transactions" => [] } }
     let(:receipts_data) { [] }
     
     before do
       allow(ethereum_client).to receive(:get_block).and_return(block_data)
       allow(ethereum_client).to receive(:get_transaction_receipts).and_return(receipts_data)
+      allow(ethereum_client).to receive(:get_block_number).and_return(10000000)
       allow(EthBlock).to receive(:from_rpc_result).and_return(instance_double(EthBlock, number: 1))
       allow(FacetBlock).to receive(:from_eth_block).and_return(instance_double(FacetBlock))
       allow(EthTransaction).to receive(:facet_txs_from_rpc_results).and_return([])
@@ -39,6 +40,10 @@
   end
   
   describe '#stats' do
+    before do
+      allow(ethereum_client).to receive(:get_block_number).and_return(10000000)
+    end
+
     it 'returns comprehensive statistics' do
       stats = prefetcher.stats
       expect(stats).to have_key(:promises_total)
@@ -50,6 +55,10 @@
   end
   
   describe '#shutdown' do
+    before do
+      allow(ethereum_client).to receive(:get_block_number).and_return(10000000)
+    end
+
     it 'shuts down gracefully' do
       expect { prefetcher.shutdown }.not_to raise_error
     end
diff --git a/spec/lib/blob_utils_spec.rb b/spec/lib/blob_utils_spec.rb
new file mode 100644
index 00000000..ceb1302b
--- /dev/null
+++ b/spec/lib/blob_utils_spec.rb
@@ -0,0 +1,180 @@
+require 'rails_helper'
+require 'blob_utils'
+
+RSpec.describe BlobUtils do
+  describe '.to_blobs' do
+    it 'encodes small data into single blob' do
+      data = "Hello, Facet!"
+      blobs = BlobUtils.to_blobs(data: data)
+      
+      expect(blobs.length).to eq(1)
+      expect(blobs.first).to start_with('0x')
+      expect(blobs.first.length).to eq(2 + BlobUtils::BYTES_PER_BLOB * 2)  # 0x + hex chars
+    end
+    
+    it 'encodes hex string data' do
+      data = "0xdeadbeef"
+      blobs = BlobUtils.to_blobs(data: data)
+      
+      expect(blobs.length).to eq(1)
+      
+      # Should be able to decode back
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      expect(decoded).to eq(data)
+    end
+    
+    it 'handles maximum size data' do
+      # Create data just under the max
+      max_data_size = BlobUtils::BYTES_PER_BLOB - 1 - BlobUtils::FIELD_ELEMENTS_PER_BLOB
+      data = "A" * max_data_size
+      
+      blobs = BlobUtils.to_blobs(data: data)
+      expect(blobs.length).to eq(1)
+    end
+    
+    it 'splits large data across multiple blobs' do
+      # Create data that requires 2 blobs
+      data = "B" * (BlobUtils::BYTES_PER_BLOB - 1000)  # Just over 1 blob
+      
+      blobs = BlobUtils.to_blobs(data: data)
+      expect(blobs.length).to eq(2)
+    end
+    
+    it 'raises error for empty data' do
+      expect { BlobUtils.to_blobs(data: '') }.to raise_error(BlobUtils::EmptyBlobError)
+      expect { BlobUtils.to_blobs(data: '0x') }.to raise_error(BlobUtils::EmptyBlobError)
+    end
+    
+    it 'raises error for data exceeding max transaction size' do
+      oversized = "C" * (BlobUtils::MAX_BYTES_PER_TRANSACTION + 1)
+      expect { BlobUtils.to_blobs(data: oversized) }.to raise_error(BlobUtils::BlobSizeTooLargeError)
+    end
+  end
+  
+  describe '.from_blobs' do
+    it 'decodes single blob back to original data' do
+      original = "Test data for Facet batches"
+      blobs = BlobUtils.to_blobs(data: original)
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      
+      expect(decoded).to eq("0x" + original.unpack1('H*'))  # Hex output for string input
+    end
+    
+    it 'decodes multiple blobs' do
+      original = "X" * 100_000  # Large enough for multiple blobs
+      blobs = BlobUtils.to_blobs(data: original)
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      
+      expect(decoded).to eq("0x" + original.unpack1('H*'))
+    end
+    
+    it 'handles hex input and output' do
+      original = "0xfacefacefacefa"  # Even-length hex string
+      blobs = BlobUtils.to_blobs(data: original)
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      
+      expect(decoded).to eq(original)
+    end
+    
+    it 'handles terminator byte correctly' do
+      # Data with 0x80 byte in it
+      data_with_80 = "\x12\x34\x80\x56\x78".b
+      blobs = BlobUtils.to_blobs(data: data_with_80)
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      
+      # Should preserve the 0x80 in the data
+      expect(decoded).to eq("0x" + data_with_80.unpack1('H*'))
+    end
+  end
+  
+  describe 'round-trip encoding with Facet batch data' do
+    it 'preserves Facet batch through blob encoding' do
+      # Create a Facet batch payload
+      magic = "\x00\x00\x00\x00\x00\x01\x23\x45"
+      batch_data = "test_batch_data"
+      length = [batch_data.length].pack('N')
+      
+      facet_payload = magic + length + batch_data
+      
+      # Encode to blob
+      blobs = BlobUtils.to_blobs(data: facet_payload)
+      
+      # Decode back
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      decoded_bytes = [decoded.sub(/^0x/, '')].pack('H*')
+      
+      # Should preserve the exact payload
+      expect(decoded_bytes).to eq(facet_payload)
+      
+      # Should be able to find magic prefix
+      expect(decoded_bytes).to include(magic)
+    end
+    
+    it 'handles aggregated data with multiple rollups' do
+      # Simulate DA Builder aggregation
+      rollup1_data = "ROLLUP_ONE_DATA"
+      facet_magic = "\x00\x00\x00\x00\x00\x01\x23\x45"
+      facet_data = "FACET_BATCH"
+      facet_payload = facet_magic + [facet_data.length].pack('N') + facet_data
+      rollup2_data = "ROLLUP_TWO_DATA"
+      
+      # Aggregate all data
+      aggregated = rollup1_data + facet_payload + rollup2_data
+      
+      # Encode to blob
+      blobs = BlobUtils.to_blobs(data: aggregated)
+      
+      # Decode back
+      decoded = BlobUtils.from_blobs(blobs: blobs)
+      decoded_bytes = [decoded.sub(/^0x/, '')].pack('H*')
+      
+      # Should find Facet data in the aggregated blob
+      expect(decoded_bytes).to include(facet_magic)
+      expect(decoded_bytes).to include(facet_data)
+      
+      # Should preserve order
+      facet_index = decoded_bytes.index(facet_magic)
+      rollup1_index = decoded_bytes.index(rollup1_data)
+      rollup2_index = decoded_bytes.index(rollup2_data)
+      
+      expect(rollup1_index).to be < facet_index
+      expect(facet_index).to be < rollup2_index
+    end
+  end
+  
+  describe 'field element constraints' do
+    it 'respects 31-byte segments with leading zeros' do
+      data = "\xFF".b * 31  # Max bytes per field element
+      blobs = BlobUtils.to_blobs(data: data)
+      blob_bytes = [blobs.first.sub(/^0x/, '')].pack('H*')
+      
+      # First byte should be 0x00 (leading zero for field element)
+      expect(blob_bytes[0].ord).to eq(0x00)
+      
+      # Next 31 bytes should be our data
+      expect(blob_bytes[1, 31]).to eq(data)
+      
+      # Then another leading zero for next field element (32nd byte)
+      expect(blob_bytes[32].ord).to eq(0x00)
+      
+      # Then terminator at position 33 (since we only have 31 bytes, the second field element just has the terminator)
+      expect(blob_bytes[33].ord).to eq(0x80)
+    end
+    
+    it 'properly pads blob to full size' do
+      data = "small"
+      blobs = BlobUtils.to_blobs(data: data)
+      blob_bytes = [blobs.first.sub(/^0x/, '')].pack('H*')
+      
+      expect(blob_bytes.length).to eq(BlobUtils::BYTES_PER_BLOB)
+      
+      # Check padding is zeros
+      # Find terminator and verify rest is zeros
+      terminator_index = blob_bytes.index("\x80".b)
+      expect(terminator_index).not_to be_nil
+      
+      padding = blob_bytes[(terminator_index + 1)..-1]
+      expect(padding.bytes.all? { |b| b == 0 }).to be true
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/mixed_transaction_types_spec.rb b/spec/mixed_transaction_types_spec.rb
new file mode 100644
index 00000000..5cb68ac9
--- /dev/null
+++ b/spec/mixed_transaction_types_spec.rb
@@ -0,0 +1,377 @@
+require 'rails_helper'
+
+RSpec.describe "Mixed Transaction Types" do
+  include FacetTransactionHelper
+  include EVMTestHelper
+  
+  let(:alice) { "0x" + "a" * 40 }
+  let(:bob) { "0x" + "b" * 40 }
+  let(:charlie) { "0x" + "c" * 40 }
+  
+  before do
+    allow(SysConfig).to receive(:facet_batch_v2_enabled?).and_return(true)
+  end
+  
+  describe "block with mixed V1 single transactions and batch transactions" do
+    it "processes both FacetTransaction and StandardL2Transaction in the same block" do
+      importer = ImporterSingleton.instance
+      current_max_eth_block = importer.current_max_eth_block
+      
+      # Use a deterministic private key for testing
+      # This will generate address: 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
+      test_private_key = "0x0000000000000000000000000000000000000000000000000000000000000001"
+      test_key = Eth::Key.new(priv: test_private_key)
+      funded_address = test_key.address.to_s
+      
+      # First, import a block with a FacetTransaction that mints funds to our test address
+      # The mint goes directly to the from_address (L1 sender) - no aliasing for EOA calldata txs
+      # We need enough data to generate sufficient mint for gas costs
+      # Each non-zero byte generates 16 units of mint at the current rate
+      funding_data = "0x" + "ff" * 5000  # 5000 non-zero bytes for plenty of mint
+      funding_payload = generate_facet_tx_payload(
+        input: funding_data,
+        to: alice,  # Can be any address, mint goes to from_address
+        gas_limit: 10_000_000,  # High gas limit for large data
+        value: 0
+      )
+
+      # Import the funding block - mint goes to from_address
+      funding_receipts = import_eth_txs([{
+        input: funding_payload,
+        from_address: funded_address,  # This address gets the mint (no aliasing for EOA)
+        to_address: EthTransaction::FACET_INBOX_ADDRESS.to_hex
+      }])
+      
+      # Verify the funding transaction succeeded
+      expect(funding_receipts.first).to be_present
+      expect(funding_receipts.first.status).to eq(1)
+      
+      # Now create our mixed transaction block
+      # Create a V1 single transaction (FacetTransaction)
+      v1_payload = generate_facet_tx_payload(
+        input: "0x12345678", # Some contract call data
+        to: alice,
+        gas_limit: 100_000,
+        value: 1000
+      )
+      
+      # Create an EIP-1559 transaction for the batch, signed by our funded address
+      # Use 0 value to avoid needing a funded balance for now
+      eip1559_tx = create_eip1559_transaction(
+        private_key: test_private_key,
+        to: charlie,
+        value: 0,  # 0 value to avoid balance requirements
+        gas_limit: 21_000
+        # Nonce will be auto-determined based on current account state
+      )
+      
+      puts "EIP-1559 tx created, length: #{eip1559_tx.to_bin.length} bytes"
+      
+      # Create a batch containing the EIP-1559 transaction
+      # Note: target_l1_block must match the block we're importing
+      # We already imported the funding block, so the next block will be +2 from original
+      target_block = current_max_eth_block.number + 2  # +2 because we imported funding block
+      batch_payload = create_batch_payload(
+        transactions: [eip1559_tx],
+        role: FacetBatchConstants::Role::PERMISSIONLESS,
+        target_l1_block: target_block
+      )
+      
+      puts "Target L1 block for batch: #{target_block}"
+      puts "Batch should contain #{[eip1559_tx].length} transaction(s)"
+      
+      # Debug the batch structure - new format has FacetBatchConstants::HEADER_SIZE bytes
+      # [MAGIC:#{FacetBatchConstants::MAGIC_SIZE}][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST]
+      test_decode = Eth::Rlp.decode(batch_payload.to_bin[FacetBatchConstants::HEADER_SIZE..-1])  # Skip header to get RLP_TX_LIST
+      puts "Decoded batch has #{test_decode.length} transactions"
+      
+      puts "Batch payload length: #{batch_payload.to_bin.length} bytes"
+      puts "Batch payload hex (first 100 chars): #{batch_payload.to_hex[0..100]}"
+      puts "Magic prefix expected: #{FacetBatchConstants::MAGIC_PREFIX.to_hex}"
+      puts "Batch contains magic? #{batch_payload.to_bin.include?(FacetBatchConstants::MAGIC_PREFIX.to_bin)}"
+      
+      # Create L1 block with both transaction types
+      eth_transactions = [
+        {
+          input: v1_payload,
+          from_address: alice,
+          to_address: EthTransaction::FACET_INBOX_ADDRESS.to_hex
+        },
+        {
+          input: batch_payload.to_hex,
+          from_address: bob,
+          to_address: "0x" + "1" * 40  # Some other address (batch can go anywhere)
+        }
+      ]
+      
+      # Import the block
+      # Temporarily increase log level to see errors
+      original_level = Rails.logger.level
+      Rails.logger.level = Logger::DEBUG
+      
+      receipts = import_eth_txs(eth_transactions)
+      
+      Rails.logger.level = original_level
+      
+      # Check if batch was collected
+      importer = ImporterSingleton.instance
+      puts "Current max eth block: #{importer.current_max_eth_block.number}"
+      
+      # Get the latest L2 block
+      latest_l2_block = EthRpcClient.l2.get_block("latest", true)
+      
+      # Debug output
+      puts "Number of receipts: #{receipts.length}"
+      puts "Number of L2 transactions: #{latest_l2_block['transactions'].length}"
+      puts "L2 transaction types: #{latest_l2_block['transactions'].map { |tx| tx['type'] }}"
+      
+      # More detailed debug
+      latest_l2_block['transactions'].each_with_index do |tx, i|
+        puts "Transaction #{i}: type=#{tx['type']}, from=#{tx['from'][0..10]}..., to=#{tx['to'] ? tx['to'][0..10] : 'nil'}..."
+      end
+      
+      # Check if facet_batch_v2_enabled is actually true
+      puts "Batch V2 enabled: #{SysConfig.facet_batch_v2_enabled?}"
+      
+      # Should have 3 transactions in the L2 block (system tx + V1 single + 1 from batch)
+      expect(latest_l2_block['transactions'].length).to eq(3)
+      
+      # Verify both transactions were included
+      tx_types = latest_l2_block['transactions'].map do |tx|
+        # tx['type'] returns a hex string like "0x7e"
+        tx['type'].to_i(16)
+      end
+      
+      # Check which transaction type we should expect based on Bluebird fork
+      expected_facet_tx_type = SysConfig.is_bluebird?(latest_l2_block['number'].to_i(16)) ? 0x7D : 0x7E
+      
+      # Should have system transaction, V1 single, and EIP-1559 (0x02)
+      expect(tx_types.count(expected_facet_tx_type)).to eq(2)  # Two FacetTransactions (system + V1 single)
+      expect(tx_types).to include(0x02)  # One EIP-1559 transaction from batch
+    end
+  end
+  
+  describe "priority batch with gas validation" do
+    before do
+      # Clear any cached state to ensure consistent test environment
+      MemeryExtensions.clear_all_caches!
+    end
+    
+    it "includes priority batch when under gas limit" do
+      importer = ImporterSingleton.instance
+      current_max_eth_block = importer.current_max_eth_block
+      
+      # Use a single test key and fund it once
+      test_key = "0x0000000000000000000000000000000000000000000000000000000000000033"
+      test_address = Eth::Key.new(priv: test_key).address.to_s
+      
+      # Fund the address with a large calldata transaction
+      funding_data = "0x" + "ff" * 5000  # Large calldata for mint
+      funding_payload = generate_facet_tx_payload(
+        input: funding_data,
+        to: alice,
+        gas_limit: 10_000_000,
+        value: 0
+      )
+      
+      funding_receipts = import_eth_txs([{
+        input: funding_payload,
+        from_address: test_address,  # This address gets the mint
+        to_address: EthTransaction::FACET_INBOX_ADDRESS.to_hex
+      }])
+      
+      expect(funding_receipts.first.status).to eq(1)
+      
+      # Update current block after funding
+      current_max_eth_block = importer.current_max_eth_block
+      
+      # Get the actual nonce for the account
+      actual_nonce = EthRpcClient.l2.get_transaction_count(test_address)
+      puts "Actual nonce for test account after funding: #{actual_nonce}"
+      base_nonce = actual_nonce  # Use actual nonce instead of assuming 1
+      
+      # Create small transactions for priority batch
+      small_txs = 3.times.map do |i|
+        create_eip1559_transaction(
+          private_key: test_key,
+          to: bob,
+          value: 0,  # Use 0 value to avoid needing more funds
+          gas_limit: 21_000,
+          nonce: base_nonce + i  # Manually increment nonce
+        )
+      end
+      
+      # Create priority batch
+      priority_batch = create_batch_payload(
+        transactions: small_txs,
+        role: FacetBatchConstants::Role::PRIORITY,
+        target_l1_block: current_max_eth_block.number + 1,
+        sign: true  # Sign for priority
+      )
+      
+      # Create forced batch with one more transaction from same account
+      forced_tx = create_eip1559_transaction(
+        private_key: test_key,
+        to: alice,
+        value: 0,
+        gas_limit: 21_000,
+        nonce: base_nonce + 3  # After the 3 priority transactions
+      )
+      
+      forced_batch = create_batch_payload(
+        transactions: [forced_tx],
+        role: FacetBatchConstants::Role::PERMISSIONLESS,
+        target_l1_block: current_max_eth_block.number + 1
+      )
+      
+      # Debug batch payloads
+      puts "Current max eth block after funding: #{current_max_eth_block.number}"
+      puts "Batches target block: #{current_max_eth_block.number + 1}"
+      puts "Priority batch length: #{priority_batch.to_bin.length} bytes"
+      puts "Priority batch hex (first 50): #{priority_batch.to_hex[0..50]}"
+      puts "Forced batch length: #{forced_batch.to_bin.length} bytes"
+      puts "Forced batch hex (first 50): #{forced_batch.to_hex[0..50]}"
+      
+      # Import blocks with both batches
+      eth_transactions = [
+        {
+          input: forced_batch.to_hex,
+          from_address: charlie,
+          to_address: "0x" + "2" * 40
+        },
+        {
+          input: priority_batch.to_hex,
+          from_address: alice,
+          to_address: "0x" + "3" * 40
+        }
+      ]
+      
+      receipts = import_eth_txs(eth_transactions)
+      latest_l2_block = EthRpcClient.l2.get_block("latest", true)
+      
+      # Debug output
+      puts "Receipts count: #{receipts.length}"
+      puts "L2 block has #{latest_l2_block['transactions'].length} transactions"
+      puts "Transaction types: #{latest_l2_block['transactions'].map { |tx| tx['type'] }}"
+      
+      # Should have 5 transactions (1 system + 3 from priority + 1 from forced)
+      expect(latest_l2_block['transactions'].length).to eq(5)
+      
+      # Priority transactions should come first after system tx
+      # Check that transactions 1-3 are from the priority batch
+      priority_txs = latest_l2_block['transactions'][1..3]
+      # These should be the EIP-1559 transactions from the priority batch
+    end
+  end
+  
+  # describe "transaction gas limit validation" do
+  #   it "excludes transactions with 0 gas limit from batches" do
+  #     importer = ImporterSingleton.instance
+  #     current_max_eth_block = importer.current_max_eth_block
+      
+  #     # Create a transaction with 0 gas limit (invalid)
+  #     test_key = "0x0000000000000000000000000000000000000000000000000000000000000001"
+  #     test_address = Eth::Key.new(priv: test_key).address.to_s
+  #     base_nonce = EthRpcClient.l2.call("eth_getTransactionCount", [test_address, "latest"]).to_i(16)
+      
+  #     zero_gas_tx = create_eip1559_transaction(
+  #       private_key: test_key,
+  #       to: bob,
+  #       value: 1000,
+  #       gas_limit: 0,  # Invalid!
+  #       nonce: base_nonce
+  #     )
+      
+  #     # Create a valid transaction
+  #     valid_tx = create_eip1559_transaction(
+  #       private_key: test_key,
+  #       to: bob,
+  #       value: 2000,
+  #       gas_limit: 21_000,
+  #       nonce: base_nonce + 1
+  #     )
+      
+  #     # Create batch with both transactions
+  #     batch = create_batch_payload(
+  #       transactions: [zero_gas_tx, valid_tx],
+  #       role: FacetBatchConstants::Role::FORCED,
+  #       target_l1_block: current_max_eth_block.number + 1
+  #     )
+      
+  #     eth_transactions = [{
+  #       input: batch.to_hex,
+  #       from_address: alice,
+  #       to_address: "0x" + "4" * 40
+  #     }]
+      
+  #     receipts = import_eth_txs(eth_transactions)
+  #     latest_l2_block = EthRpcClient.l2.get_block("latest", true)
+      
+  #     # Should only have 1 transaction (the valid one)
+  #     expect(latest_l2_block['transactions'].length).to eq(1)
+      
+  #     # Verify it's the valid transaction
+  #     tx = latest_l2_block['transactions'].first
+  #     expect(tx['value'].to_i(16)).to eq(2000)
+  #   end
+  # end
+  
+  private
+  
+  def create_eip1559_transaction(private_key:, to:, value:, gas_limit:, nonce: nil)
+    chain_id = ChainIdManager.current_l2_chain_id
+    
+    # Use Eth library's built-in transaction support
+    key = Eth::Key.new(priv: private_key)
+    
+    # Auto-determine nonce if not provided
+    if nonce.nil?
+      address = key.address.to_s
+      nonce = EthRpcClient.l2.call("eth_getTransactionCount", [address, "latest"]).to_i(16)
+    end
+    
+    # Create an EIP-1559 transaction using the Eth library
+    tx = Eth::Tx::Eip1559.new({
+      chain_id: chain_id,
+      nonce: nonce,
+      priority_fee: 1 * Eth::Unit::GWEI,  # 1 gwei as priority fee
+      max_gas_fee: 2 * Eth::Unit::GWEI,   # 2 gwei as max fee
+      gas_limit: gas_limit,
+      to: to,
+      value: value,
+      data: ""  # empty data for simple transfer
+    })
+    
+    # Sign the transaction
+    tx.sign(key)
+    
+    # Get the raw signed transaction bytes (add 0x prefix if missing)
+    hex_str = tx.hex
+    hex_str = "0x#{hex_str}" unless hex_str.start_with?('0x')
+    ByteString.from_hex(hex_str)
+  end
+  
+  def create_batch_payload(transactions:, role:, target_l1_block:, sign: false)
+    chain_id = ChainIdManager.current_l2_chain_id
+
+    # Create RLP-encoded transaction list
+    rlp_tx_list = Eth::Rlp.encode(transactions.map(&:to_bin))
+
+    # Build wire format: [MAGIC:#{FacetBatchConstants::MAGIC_SIZE}][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST][SIGNATURE:65]?
+    payload = FacetBatchConstants::MAGIC_PREFIX.to_bin
+    payload += [chain_id].pack('Q>')  # uint64 big-endian
+    payload += [FacetBatchConstants::VERSION].pack('C')
+    payload += [role].pack('C')
+    payload += [rlp_tx_list.length].pack('N')
+    payload += rlp_tx_list
+
+    # Add signature for priority batches
+    if sign && role == FacetBatchConstants::Role::PRIORITY
+      # Add dummy signature for priority batches (65 bytes: r:32, s:32, v:1)
+      signature = "\x00" * 32 + "\x00" * 32 + "\x01"
+      payload += signature
+    end
+
+    ByteString.from_bin(payload)
+  end
+end
diff --git a/spec/models/standard_l2_transaction_signature_recovery_spec.rb b/spec/models/standard_l2_transaction_signature_recovery_spec.rb
new file mode 100644
index 00000000..5fb03a61
--- /dev/null
+++ b/spec/models/standard_l2_transaction_signature_recovery_spec.rb
@@ -0,0 +1,237 @@
+require 'rails_helper'
+
+RSpec.describe StandardL2Transaction do
+  describe 'signature recovery' do
+    let(:private_key) { Eth::Key.new }
+    let(:from_address) { private_key.address.to_s }
+    let(:to_address) { "0x70997970c51812dc003a010c7d01b50e0d17dc79" }
+    let(:chain_id) { 0xface7b }
+    
+    describe '.recover_address_eip1559' do
+      it 'recovers the correct address from EIP-1559 transaction' do
+        # Create transaction data
+        tx_data = [
+          chain_id,                    # chainId
+          1,                           # nonce  
+          100000,                      # maxPriorityFeePerGas
+          200000,                      # maxFeePerGas
+          21000,                       # gasLimit
+          to_address,                  # to
+          1000000,                     # value
+          "",                          # data
+          []                           # accessList
+        ]
+        
+        # Create signing hash (EIP-1559 uses type 2)
+        encoded = "\x02" + Eth::Rlp.encode(tx_data)
+        signing_hash = Eth::Util.keccak256(encoded)
+        
+        # Sign with private key (returns hex string with r, s, v)
+        signature_hex = private_key.sign(signing_hash)
+        # The signature is hex encoded: remove 0x prefix if present
+        signature_hex = signature_hex.sub(/^0x/, '')
+        
+        # Extract r, s, v from hex signature
+        r_hex = signature_hex[0...64]
+        s_hex = signature_hex[64...128]
+        v_hex = signature_hex[128..]
+        
+        # Convert to binary for our method
+        r = [r_hex].pack('H*')
+        s = [s_hex].pack('H*')
+        v_raw = v_hex.to_i(16)
+        
+        # For EIP-1559, v should be 0 or 1
+        # The signature has v=27 or v=28 from legacy format, convert to 0 or 1
+        v = v_raw - 27
+        
+        # Recover address using our method
+        decoded = tx_data
+        recovered = StandardL2Transaction.recover_address_eip1559(decoded, v, r, s, chain_id)
+        
+        expect(recovered.to_hex.downcase).to eq(from_address.downcase)
+      end
+      
+      it 'handles v values of 0 and 1 correctly' do
+        tx_data = [chain_id, 1, 100000, 200000, 21000, to_address, 1000000, "", []]
+        encoded = "\x02" + Eth::Rlp.encode(tx_data)
+        signing_hash = Eth::Util.keccak256(encoded)
+        
+        # Test with v = 0
+        signature_hex = private_key.sign(signing_hash)
+        signature_hex = signature_hex.sub(/^0x/, '')
+        r_hex = signature_hex[0...64]
+        s_hex = signature_hex[64...128]
+        r = [r_hex].pack('H*')
+        s = [s_hex].pack('H*')
+        
+        recovered_v0 = StandardL2Transaction.recover_address_eip1559(tx_data, 0, r, s, chain_id)
+        recovered_v1 = StandardL2Transaction.recover_address_eip1559(tx_data, 1, r, s, chain_id)
+        
+        # One of them should match the correct address
+        addresses = [recovered_v0.to_hex.downcase, recovered_v1.to_hex.downcase]
+        expect(addresses).to include(from_address.downcase)
+      end
+      
+      it 'returns null address on recovery failure' do
+        # Invalid signature data
+        invalid_r = "\x00" * 32
+        invalid_s = "\x00" * 32
+        tx_data = [chain_id, 1, 100000, 200000, 21000, to_address, 1000000, "", []]
+        
+        expect { StandardL2Transaction.recover_address_eip1559(tx_data, 0, invalid_r, invalid_s, chain_id) }.to raise_error(StandardL2Transaction::DecodeError)
+      end
+    end
+    
+    describe '.recover_address_eip2930' do
+      it 'recovers the correct address from EIP-2930 transaction' do
+        # Create transaction data  
+        tx_data = [
+          chain_id,                    # chainId
+          1,                           # nonce
+          100000,                      # gasPrice
+          21000,                       # gasLimit
+          to_address,                  # to
+          1000000,                     # value
+          "",                          # data
+          []                           # accessList
+        ]
+        
+        # Create signing hash (EIP-2930 uses type 1)
+        encoded = "\x01" + Eth::Rlp.encode(tx_data)
+        signing_hash = Eth::Util.keccak256(encoded)
+        
+        # Sign with private key (returns hex string with r, s, v)
+        signature_hex = private_key.sign(signing_hash)
+        # The signature is hex encoded: remove 0x prefix if present
+        signature_hex = signature_hex.sub(/^0x/, '')
+        
+        # Extract r, s, v from hex signature
+        r_hex = signature_hex[0...64]
+        s_hex = signature_hex[64...128]
+        v_hex = signature_hex[128..]
+        
+        # Convert to binary for our method
+        r = [r_hex].pack('H*')
+        s = [s_hex].pack('H*')
+        v = v_hex.to_i(16)
+        
+        # Recover address using our method
+        recovered = StandardL2Transaction.recover_address_eip2930(tx_data, v, r, s, chain_id)
+        
+        expect(recovered.to_hex.downcase).to eq(from_address.downcase)
+      end
+    end
+    
+    describe '.recover_address_legacy' do
+      it 'recovers the correct address from legacy transaction with EIP-155' do
+        # For EIP-155, we need to differentiate between:
+        # 1. The transaction data used for signing (includes chain_id, empty r, empty s)
+        # 2. The transaction data stored/transmitted (just the basic fields)
+        
+        # Basic transaction data (what gets stored)
+        tx_data_basic = [
+          1,                           # nonce
+          100000,                      # gasPrice
+          21000,                       # gasLimit
+          to_address,                  # to
+          1000000,                     # value
+          ""                           # data
+        ]
+        
+        # For EIP-155 signing, append chain_id and empty r,s
+        tx_data_for_signing = tx_data_basic + [chain_id, "", ""]
+        
+        # Create signing hash with EIP-155 fields
+        encoded = Eth::Rlp.encode(tx_data_for_signing)
+        signing_hash = Eth::Util.keccak256(encoded)
+        
+        # Sign with private key with chain_id for EIP-155
+        signature_hex = private_key.sign(signing_hash, chain_id)
+        signature_hex = signature_hex.sub(/^0x/, '')
+        
+        # Extract r, s, v from hex signature
+        r_hex = signature_hex[0...64]
+        s_hex = signature_hex[64...128]
+        v_hex = signature_hex[128..]
+        
+        # Convert to binary for our method
+        r = [r_hex].pack('H*')
+        s = [s_hex].pack('H*')
+        v = v_hex.to_i(16)  # This will be 2*chain_id + 35 + recovery_id
+        
+        # Our recovery method needs to handle EIP-155 internally
+        # It should reconstruct the signing data with chain_id when v >= 35
+        recovered = StandardL2Transaction.recover_address_legacy(tx_data_basic, v, r, s)
+        
+        expect(recovered.to_hex.downcase).to eq(from_address.downcase)
+      end
+      
+      it 'recovers the correct address from pre-EIP-155 legacy transaction' do
+        # Create transaction data without EIP-155
+        tx_data = [
+          1,                           # nonce
+          100000,                      # gasPrice
+          21000,                       # gasLimit
+          to_address,                  # to
+          1000000,                     # value
+          ""                           # data
+        ]
+        
+        # Create signing hash
+        encoded = Eth::Rlp.encode(tx_data)
+        signing_hash = Eth::Util.keccak256(encoded)
+        
+        # Sign with private key (returns hex string with r, s, v)
+        signature_hex = private_key.sign(signing_hash)
+        signature_hex = signature_hex.sub(/^0x/, '')
+        
+        # Extract r, s, v from hex signature
+        r_hex = signature_hex[0...64]
+        s_hex = signature_hex[64...128]
+        v_hex = signature_hex[128..]
+        
+        # Convert to binary for our method
+        r = [r_hex].pack('H*')
+        s = [s_hex].pack('H*')
+        v = v_hex.to_i(16)  # v is already 27 or 28 for legacy
+        
+        # Recover address using our method
+        recovered = StandardL2Transaction.recover_address_legacy(tx_data, v, r, s)
+        
+        expect(recovered.to_hex.downcase).to eq(from_address.downcase)
+      end
+    end
+    
+    describe 'integration with Eth::Signature module' do
+      it 'uses Eth::Signature.recover instead of instantiating Eth::Signature' do
+        # This test verifies we're using the module method, not trying to instantiate
+        expect(Eth::Signature).to respond_to(:recover)
+        expect { Eth::Signature.new }.to raise_error(NoMethodError)
+      end
+      
+      it 'passes correct parameters to Eth::Signature.recover' do
+        tx_data = [chain_id, 1, 100000, 200000, 21000, to_address, 1000000, "", []]
+        encoded = "\x02" + Eth::Rlp.encode(tx_data)
+        signing_hash = Eth::Util.keccak256(encoded)
+        
+        signature_hex = private_key.sign(signing_hash)
+        signature_hex = signature_hex.sub(/^0x/, '')
+        r = [signature_hex[0...64]].pack('H*')
+        s = [signature_hex[64...128]].pack('H*')
+        v = signature_hex[128..].to_i(16) - 27  # Convert to 0 or 1 for EIP-1559
+        
+        # Mock to verify correct parameters
+        # Our implementation passes a hex string for signature
+        expected_signature = r.unpack1('H*') + s.unpack1('H*') + v.to_s(16).rjust(2, '0')
+        expect(Eth::Signature).to receive(:recover).with(
+          signing_hash,
+          expected_signature,
+          chain_id
+        ).and_call_original
+        
+        StandardL2Transaction.recover_address_eip1559(tx_data, v, r, s, chain_id)
+      end
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/revert_spec.rb b/spec/revert_spec.rb
index 978d0a61..612d2f69 100644
--- a/spec/revert_spec.rb
+++ b/spec/revert_spec.rb
@@ -21,27 +21,27 @@
     res.contract_address
   }
   
-  it do
-    airdrop_address = deploy_contract_with_proxy(
-      implementation: 'predeploys/AirdropERC20Vb02',
-      from: from_address,
-      args: [
-        "Facet Cards",
-        "Card",
-        from_address,
-        18,
-        100.ether,
-        1.ether
-      ]
-    ).contract_address
+  # it do
+  #   airdrop_address = deploy_contract_with_proxy(
+  #     implementation: 'predeploys/AirdropERC20Vb02',
+  #     from: from_address,
+  #     args: [
+  #       "Facet Cards",
+  #       "Card",
+  #       from_address,
+  #       18,
+  #       100.ether,
+  #       1.ether
+  #     ]
+  #   ).contract_address
     
-    create_and_import_block(
-      facet_data: "0x7b227461626c65223a7b22616d6f756e74223a22323030303030303030303030303030303039353239343538363838227d7d",
-      to_address: airdrop_address,
-      from_address: from_address,
-      expect_failure: true
-    )
-  end
+  #   create_and_import_block(
+  #     facet_data: "0x7b227461626c65223a7b22616d6f756e74223a22323030303030303030303030303030303039353239343538363838227d7d",
+  #     to_address: airdrop_address,
+  #     from_address: from_address,
+  #     expect_failure: true
+  #   )
+  # end
   
   it 'handles reverts' do
     call_contract_function(
diff --git a/spec/services/batch_signature_verifier_spec.rb b/spec/services/batch_signature_verifier_spec.rb
new file mode 100644
index 00000000..3063a4d9
--- /dev/null
+++ b/spec/services/batch_signature_verifier_spec.rb
@@ -0,0 +1,60 @@
+require 'rails_helper'
+
+RSpec.describe BatchSignatureVerifier do
+  let(:chain_id) { ChainIdManager.current_l2_chain_id }
+  let(:verifier) { described_class.new(chain_id: chain_id) }
+  let(:key) { Eth::Key.new }
+
+  def build_signed_data(role: FacetBatchConstants::Role::PRIORITY)
+    tx_list = Eth::Rlp.encode([])
+    [
+      [chain_id].pack('Q>'),
+      [FacetBatchConstants::VERSION].pack('C'),
+      [role].pack('C'),
+      tx_list
+    ].join
+  end
+
+  def build_signature(message_hash)
+    signature_hex = key.sign(message_hash).sub(/^0x/, '')
+    [signature_hex].pack('H*')
+  end
+
+  describe '#verify_wire_format' do
+    it 'accepts signatures with legacy v values (27/28)' do
+      signed_data = build_signed_data
+      message_hash = Eth::Util.keccak256(signed_data)
+      signature = build_signature(message_hash)
+
+      signer = verifier.verify_wire_format(signed_data, signature)
+
+      expect(signer.to_hex.downcase).to eq(key.address.to_s.downcase)
+    end
+
+    it 'accepts signatures with normalised v values (0/1)' do
+      signed_data = build_signed_data
+      message_hash = Eth::Util.keccak256(signed_data)
+      signature = build_signature(message_hash)
+
+      normalised_signature = signature.dup
+      normalised_signature.setbyte(64, normalised_signature.getbyte(64) - 27)
+
+      signer = verifier.verify_wire_format(signed_data, normalised_signature)
+
+      expect(signer.to_hex.downcase).to eq(key.address.to_s.downcase)
+    end
+
+    it 'returns nil for signatures with invalid recovery ids' do
+      signed_data = build_signed_data
+      message_hash = Eth::Util.keccak256(signed_data)
+      signature = build_signature(message_hash)
+
+      invalid_signature = signature.dup
+      invalid_signature.setbyte(64, 5)
+
+      signer = verifier.verify_wire_format(signed_data, invalid_signature)
+
+      expect(signer).to be_nil
+    end
+  end
+end
diff --git a/spec/services/blob_aggregation_spec.rb b/spec/services/blob_aggregation_spec.rb
new file mode 100644
index 00000000..a38da020
--- /dev/null
+++ b/spec/services/blob_aggregation_spec.rb
@@ -0,0 +1,201 @@
+require 'rails_helper'
+require 'support/blob_test_helper'
+
+RSpec.describe 'Blob Aggregation Scenarios' do
+  include BlobTestHelper
+  
+  describe 'DA Builder aggregation patterns' do
+    let(:parser) { FacetBatchParser.new }
+    
+    it 'finds Facet batch at start of aggregated blob' do
+      blob = create_test_blob_with_facet_data(position: :start)
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches.length).to eq(1)
+      expect(batches.first.transactions).not_to be_empty
+    end
+    
+    it 'finds Facet batch in middle of aggregated blob' do
+      blob = create_test_blob_with_facet_data(position: :middle)
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches.length).to eq(1)
+      expect(batches.first.source).to eq(FacetBatchConstants::Source::BLOB)
+    end
+    
+    it 'finds Facet batch at end of aggregated blob' do
+      blob = create_test_blob_with_facet_data(position: :end)
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches.length).to eq(1)
+    end
+    
+    it 'finds multiple Facet batches in single blob' do
+      blob = create_test_blob_with_facet_data(position: :multiple)
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches.length).to eq(2)
+      # Should be in order they appear in blob
+      expect(batches[0].l1_tx_index).to eq(0)
+      expect(batches[1].l1_tx_index).to eq(0)
+    end
+    
+    it 'handles blob with no Facet data' do
+      # Simulate other rollup's data only
+      blob = ByteString.from_bin("\xDE\xAD\xBE\xEF".b * 32_768)  # 128KB of non-Facet data
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches).to be_empty
+    end
+    
+    it 'handles corrupted magic prefix' do
+      # Create blob with almost-correct magic
+      bad_magic = "\x00\x00\x00\x00\x00\x01\x23\x46".b  # One byte off
+      blob_data = bad_magic + [100].pack('N') + ("\x00".b * 100)
+      blob_data += "\x00".b * (131_072 - blob_data.length)
+      
+      blob = ByteString.from_bin(blob_data)
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches).to be_empty
+    end
+    
+    it 'handles batch that claims size beyond blob boundary' do
+      # Create batch header that claims to be huge
+      chain_id = ChainIdManager.current_l2_chain_id
+
+      blob_data = FacetBatchConstants::MAGIC_PREFIX.to_bin
+      blob_data += [chain_id].pack('Q>')  # uint64 big-endian
+      blob_data += [FacetBatchConstants::VERSION].pack('C')
+      blob_data += [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+      blob_data += [200_000].pack('N')  # Claims 200KB but blob is only 128KB
+      blob_data += "\x00".b * (131_072 - blob_data.length)
+      
+      blob = ByteString.from_bin(blob_data)
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches).to be_empty  # Should reject invalid size
+    end
+  end
+  
+  describe 'Round-trip encoding' do
+    it 'survives encode -> blob -> parse cycle' do
+      # Create test transactions
+      transactions = 3.times.map do |i|
+        create_test_transaction(nonce: i, value: 1000 * (i + 1))
+      end
+      
+      # Create batch in new wire format
+      chain_id = ChainIdManager.current_l2_chain_id
+
+      # Create RLP-encoded transaction list
+      rlp_tx_list = Eth::Rlp.encode(transactions.map(&:to_bin))
+
+      # Construct wire format: [MAGIC:#{FacetBatchConstants::MAGIC_SIZE}][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST]
+      payload = FacetBatchConstants::MAGIC_PREFIX.to_bin
+      payload += [chain_id].pack('Q>')  # uint64 big-endian
+      payload += [FacetBatchConstants::VERSION].pack('C')
+      payload += [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+      payload += [rlp_tx_list.length].pack('N')
+      payload += rlp_tx_list
+      
+      # Embed in blob
+      blob_data = payload + ("\x00".b * (131_072 - payload.length))
+      blob = ByteString.from_bin(blob_data)
+      
+      # Parse back
+      parser = FacetBatchParser.new
+      parsed_batches = parser.parse_payload(
+        blob,
+        0,      # l1_tx_index
+        FacetBatchConstants::Source::BLOB
+      )
+
+      expect(parsed_batches.length).to eq(1)
+      parsed = parsed_batches.first
+
+      expect(parsed.role).to eq(FacetBatchConstants::Role::PERMISSIONLESS)
+      expect(parsed.chain_id).to eq(chain_id)
+      expect(parsed.transactions.length).to eq(3)
+      expect(parsed.transactions.map(&:to_bin)).to eq(transactions.map(&:to_bin))
+    end
+  end
+  
+  describe 'Property tests' do
+    it 'handles random payloads up to 128KB' do
+      100.times do
+        # Generate random size
+        size = rand(100..120_000)
+        
+        # Generate random transactions
+        tx_count = rand(1..10)
+        transactions = tx_count.times.map do |i|
+          create_test_transaction(nonce: i, value: rand(0..10000))
+        end
+        
+        # Create batch with random data
+        batch_data = create_test_batch_data(transactions)
+        
+        # Add to blob with random position
+        position = [:start, :middle, :end].sample
+        blob = create_test_blob_with_facet_data(
+          transactions: transactions,
+          position: position
+        )
+        
+        # Should be able to extract
+        batches = extract_facet_batches_from_blob(blob)
+        
+        expect(batches).not_to be_empty
+        expect(batches.first.transactions.length).to eq(transactions.length)
+      end
+    end
+    
+    it 'correctly handles maximum blob utilization' do
+      # Try to pack as much as possible into a blob
+      transactions = []
+      total_size = 0
+      
+      # Keep adding transactions until we approach the limit
+      while total_size < 100_000  # Leave room for encoding overhead
+        tx = create_test_transaction(nonce: transactions.length)
+        tx_size = tx.to_bin.length
+        
+        break if total_size + tx_size + 100 > 120_000  # Safety margin
+        
+        transactions << tx
+        total_size += tx_size
+      end
+      
+      # Create blob with maximum transactions
+      blob = create_test_blob_with_facet_data(transactions: transactions)
+      
+      # Should successfully extract all transactions
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches.length).to eq(1)
+      expect(batches.first.transactions.length).to eq(transactions.length)
+    end
+  end
+  
+  describe 'Beacon API response handling' do
+    it 'parses beacon blob sidecar format' do
+      # Create test blob
+      blob_data = create_test_blob_with_facet_data
+      
+      # Create beacon API response
+      sidecar = create_beacon_blob_sidecar_response(blob_data, slot: 5000)
+      
+      # Extract blob data from sidecar
+      decoded_blob = Base64.decode64(sidecar["blob"])
+      
+      expect(decoded_blob.length).to eq(131_072)  # Full blob size
+      
+      # Should find Facet data
+      blob = ByteString.from_bin(decoded_blob)
+      batches = extract_facet_batches_from_blob(blob)
+      
+      expect(batches).not_to be_empty
+    end
+  end
+end
diff --git a/spec/services/blob_provider_spec.rb b/spec/services/blob_provider_spec.rb
new file mode 100644
index 00000000..0157269a
--- /dev/null
+++ b/spec/services/blob_provider_spec.rb
@@ -0,0 +1,250 @@
+require 'rails_helper'
+require 'support/blob_test_helper'
+
+RSpec.describe BlobProvider do
+  include BlobTestHelper
+  
+  describe 'blob fetching' do
+    let(:provider) { described_class.new }
+    let(:versioned_hash) { "0x01" + "a" * 62 }
+    
+    describe '#get_blob' do
+      context 'when blob contains Facet data' do
+        let(:test_transactions) { [create_test_transaction(value: 1000)] }
+        let(:blob_data) { create_test_blob_with_facet_data(transactions: test_transactions, position: :middle) }
+        
+        before do
+          # Stub beacon API response (now includes block_data parameter)
+          allow(provider).to receive(:fetch_blob_from_beacon).with(versioned_hash, block_number: 12345, block_data: nil).and_return(blob_data)
+        end
+        
+        it 'returns the decoded data from the blob' do
+          result = provider.get_blob(versioned_hash, block_number: 12345)
+          
+          # The provider returns decoded data, not the raw blob
+          # Decode the test blob to compare
+          decoded_test_data = BlobUtils.from_blobs(blobs: [blob_data.to_hex])
+          expected = ByteString.from_hex(decoded_test_data)
+          
+          expect(result).to eq(expected)
+        end
+        
+      end
+      
+      context 'when blob does not contain Facet data' do
+        let(:non_facet_data) { "\xFF".b * 10_000 }  # Some non-Facet data
+        let(:blob_data) do
+          # Encode the non-Facet data into a proper blob
+          blobs = BlobUtils.to_blobs(data: non_facet_data)
+          ByteString.from_hex(blobs.first)
+        end
+        
+        before do
+          allow(provider).to receive(:fetch_blob_from_beacon).with(versioned_hash, block_number: 12345, block_data: nil).and_return(blob_data)
+        end
+        
+        it 'still returns the decoded data (provider is content-agnostic)' do
+          result = provider.get_blob(versioned_hash, block_number: 12345)
+          
+          # The provider should return the decoded data regardless of content
+          decoded_test_data = BlobUtils.from_blobs(blobs: [blob_data.to_hex])
+          expected = ByteString.from_hex(decoded_test_data)
+          
+          expect(result).to eq(expected)
+          expect(result).not_to be_nil
+        end
+      end
+      
+      context 'when beacon API is unavailable' do
+        before do
+          allow(provider).to receive(:fetch_blob_from_beacon).with(anything, hash_including(:block_number)).and_raise(Net::HTTPError.new("Connection failed", nil))
+        end
+        
+        it 'raises the error' do
+          expect {
+            provider.get_blob(versioned_hash, block_number: 12345)
+          }.to raise_error(Net::HTTPError, "Connection failed")
+        end
+      end
+    end
+    
+    describe '#list_carriers' do
+      let(:block_number) { 12345 }
+      let(:ethereum_client) { instance_double(EthRpcClient) }
+      
+      before do
+        allow(provider).to receive(:ethereum_client).and_return(ethereum_client)
+      end
+      
+      context 'with blob transactions in block' do
+        let(:block_result) do
+          {
+            'number' => "0x#{block_number.to_s(16)}",
+            'transactions' => [
+              {
+                'hash' => '0x' + '1' * 64,
+                'transactionIndex' => '0x0',
+                'type' => '0x03',  # EIP-4844 type
+                'blobVersionedHashes' => ['0x01' + 'a' * 62, '0x01' + 'b' * 62]
+              },
+              {
+                'hash' => '0x' + '2' * 64,
+                'transactionIndex' => '0x1',
+                'type' => '0x02'  # Regular EIP-1559 - no blobs
+              },
+              {
+                'hash' => '0x' + '3' * 64,
+                'transactionIndex' => '0x2',
+                'type' => '0x03',  # Another blob tx
+                'blobVersionedHashes' => ['0x01' + 'c' * 62]
+              }
+            ]
+          }
+        end
+        
+        before do
+          allow(ethereum_client).to receive(:get_block).with(block_number, true).and_return(block_result)
+        end
+        
+        it 'returns carriers with blob versioned hashes' do
+          carriers = provider.list_carriers(block_number)
+          
+          expect(carriers.length).to eq(2)  # Only blob transactions
+          
+          expect(carriers[0]).to eq({
+            tx_hash: '0x' + '1' * 64,
+            tx_index: 0,
+            versioned_hashes: ['0x01' + 'a' * 62, '0x01' + 'b' * 62]
+          })
+          
+          expect(carriers[1]).to eq({
+            tx_hash: '0x' + '3' * 64,
+            tx_index: 2,
+            versioned_hashes: ['0x01' + 'c' * 62]
+          })
+        end
+      end
+      
+      context 'with no blob transactions' do
+        let(:block_result) do
+          {
+            'number' => "0x#{block_number.to_s(16)}",
+            'transactions' => [
+              { 'hash' => '0x' + '1' * 64, 'type' => '0x02' }
+            ]
+          }
+        end
+        
+        before do
+          allow(ethereum_client).to receive(:get_block).with(block_number, true).and_return(block_result)
+        end
+        
+        it 'returns empty array' do
+          carriers = provider.list_carriers(block_number)
+          expect(carriers).to be_empty
+        end
+      end
+    end
+  end
+  
+  describe 'Integration with FacetBatchCollector' do
+    include BlobTestHelper
+    
+    let(:block_number) { 12345 }
+    let(:versioned_hash) { "0x01" + "f" * 62 }
+    
+    xit 'successfully extracts Facet batches from aggregated blob (TODO: fix integration test)' do
+      # Create a blob with Facet data in the middle (simulating DA Builder aggregation)
+      aggregated_blob = create_test_blob_with_facet_data(position: :middle)
+      
+      # Stub the beacon provider
+      beacon_provider = stub_beacon_blob_response(versioned_hash, aggregated_blob)
+      
+      # Set up a transaction that carries a blob
+      tx_hash = '0x' + 'a' * 64
+      
+      # Create a blob transaction (type 3)
+      blob_tx = {
+        'hash' => tx_hash,
+        'transactionIndex' => '0x0',
+        'type' => '0x3',  # Blob transaction
+        'from' => '0x' + 'b' * 40,
+        'to' => '0x' + 'c' * 40,
+        'input' => '0x'
+      }
+      
+      # Create receipt with blob versioned hashes
+      receipt = {
+        'transactionHash' => tx_hash,
+        'transactionIndex' => '0x0',
+        'status' => '0x1',  # Success
+        'blobVersionedHashes' => [versioned_hash],
+        'logs' => []
+      }
+      
+      eth_block = {
+        'number' => "0x#{block_number.to_s(16)}",
+        'transactions' => [blob_tx]
+      }
+      
+      collector = FacetBatchCollector.new(
+        eth_block: eth_block,
+        receipts: [receipt],
+        blob_provider: beacon_provider
+      )
+      
+      # Simulate list_carriers returning our blob
+      allow(beacon_provider).to receive(:list_carriers).with(block_number).and_return([
+        {
+          tx_hash: tx_hash,
+          tx_index: 0,
+          versioned_hashes: [versioned_hash]
+        }
+      ])
+      
+      # Collect should find our batch
+      result = collector.call
+      
+      expect(result.batches).not_to be_empty
+      expect(result.stats[:batches_blobs]).to eq(1)
+    end
+    
+    it 'handles multiple Facet batches in single blob' do
+      blob_data = create_test_blob_with_facet_data(position: :multiple)
+      
+      # Extract batches using parser
+      batches = extract_facet_batches_from_blob(blob_data)
+      
+      expect(batches.length).to eq(2)
+      expect(batches.all? { |b| b.is_a?(ParsedBatch) }).to be true
+    end
+    
+    it 'handles missing blobs gracefully' do
+      beacon_provider = stub_beacon_blob_response(versioned_hash, nil)  # Blob not found
+      
+      eth_block = {
+        'number' => "0x#{block_number.to_s(16)}",
+        'transactions' => []
+      }
+      
+      collector = FacetBatchCollector.new(
+        eth_block: eth_block,
+        receipts: [],
+        blob_provider: beacon_provider
+      )
+      
+      allow(beacon_provider).to receive(:list_carriers).with(block_number).and_return([
+        {
+          tx_hash: '0x' + 'a' * 64,
+          tx_index: 0,
+          versioned_hashes: [versioned_hash]
+        }
+      ])
+      
+      result = collector.call
+      
+      expect(result.batches).to be_empty
+      expect(result.stats[:missing_blobs]).to eq(1)
+    end
+  end
+end
\ No newline at end of file
diff --git a/spec/services/facet_batch_collector_spec.rb b/spec/services/facet_batch_collector_spec.rb
new file mode 100644
index 00000000..f455b58d
--- /dev/null
+++ b/spec/services/facet_batch_collector_spec.rb
@@ -0,0 +1,280 @@
+require 'rails_helper'
+
+RSpec.describe FacetBatchCollector do
+  let(:block_number) { 12345 }
+  let(:eth_block) do
+    {
+      'number' => "0x#{block_number.to_s(16)}",
+      'hash' => '0x' + 'a' * 64,
+      'transactions' => transactions
+    }
+  end
+  
+  let(:transactions) { [] }
+  let(:receipts) { [] }
+  let(:blob_provider) { BlobProvider.new }
+  
+  let(:collector) do
+    described_class.new(
+      eth_block: eth_block,
+      receipts: receipts,
+      blob_provider: blob_provider
+    )
+  end
+  
+  before do
+    allow(SysConfig).to receive(:facet_batch_v2_enabled?).and_return(true)
+  end
+  
+  describe '#call' do
+    context 'with V1 calldata transaction' do
+      let(:transactions) do
+        [{
+          'hash' => '0x' + 'b' * 64,
+          'transactionIndex' => '0x0',
+          'to' => EthTransaction::FACET_INBOX_ADDRESS.to_hex,
+          'input' => create_v1_tx_payload
+        }]
+      end
+      
+      let(:receipts) do
+        [{
+          'transactionHash' => transactions[0]['hash'],
+          'status' => '0x1',
+          'logs' => []
+        }]
+      end
+      
+      it 'collects V1 single transaction' do
+        result = collector.call
+        
+        expect(result.single_txs.length).to eq(1)
+        expect(result.batches).to be_empty
+        
+        single = result.single_txs.first
+        expect(single[:source]).to eq('calldata')
+        expect(single[:tx_hash]).to eq(transactions[0]['hash'])
+      end
+    end
+    
+    context 'with V1 event transaction' do
+      let(:transactions) do
+        [{
+          'hash' => '0x' + 'c' * 64,
+          'transactionIndex' => '0x0',
+          'to' => '0x' + 'd' * 40,
+          'input' => '0x'
+        }]
+      end
+      
+      let(:receipts) do
+        [{
+          'transactionHash' => transactions[0]['hash'],
+          'status' => '0x1',
+          'logs' => [{
+            'removed' => false,
+            'topics' => [EthTransaction::FacetLogInboxEventSig.to_hex],
+            'data' => create_v1_tx_payload,
+            'address' => '0x' + 'e' * 40,
+            'logIndex' => '0x0'
+          }]
+        }]
+      end
+      
+      it 'collects V1 event transaction' do
+        result = collector.call
+        
+        expect(result.single_txs.length).to eq(1)
+        expect(result.batches).to be_empty
+        
+        single = result.single_txs.first
+        expect(single[:source]).to eq('events')
+        expect(single[:events].length).to eq(1)
+      end
+    end
+    
+    context 'with batch in calldata' do
+      let(:batch_payload) { create_batch_payload }
+      
+      let(:transactions) do
+        [{
+          'hash' => '0x' + 'f' * 64,
+          'transactionIndex' => '0x0',
+          'to' => '0x' + '1' * 40,
+          'input' => batch_payload.to_hex
+        }]
+      end
+      
+      let(:receipts) do
+        [{
+          'transactionHash' => transactions[0]['hash'],
+          'status' => '0x1',
+          'logs' => []
+        }]
+      end
+      
+      it 'collects batch from calldata' do
+        result = collector.call
+        
+        expect(result.single_txs).to be_empty
+        expect(result.batches.length).to eq(1)
+        
+        batch = result.batches.first
+        expect(batch.source).to eq(FacetBatchConstants::Source::CALLDATA)
+        expect(batch.l1_tx_index).to eq(0)
+      end
+    end
+    
+    context 'with batch in event' do
+      let(:batch_payload) { create_batch_payload }
+      
+      let(:transactions) do
+        [{
+          'hash' => '0x' + '2' * 64,
+          'transactionIndex' => '0x0',
+          'to' => '0x' + '3' * 40,
+          'input' => '0x'
+        }]
+      end
+      
+      let(:receipts) do
+        [{
+          'transactionHash' => transactions[0]['hash'],
+          'status' => '0x1',
+          'logs' => [{
+            'removed' => false,
+            'topics' => [EthTransaction::FacetLogInboxEventSig.to_hex],
+            'data' => batch_payload.to_hex,
+            'address' => '0x' + '4' * 40,
+            'logIndex' => '0x0'
+          }]
+        }]
+      end
+      
+      it 'does not collect batch from event (batches not supported in events)' do
+        result = collector.call
+        
+        # V2 batches are NOT supported in events - only calldata and blobs
+        expect(result.single_txs).to be_empty
+        expect(result.batches).to be_empty
+      end
+    end
+    
+    context 'with duplicate batches across calldata' do
+      let(:batch_payload) { create_batch_payload }
+      
+      let(:transactions) do
+        [
+          {
+            'hash' => '0x' + '5' * 64,
+            'transactionIndex' => '0x0',
+            'to' => '0x' + '6' * 40,
+            'input' => batch_payload.to_hex  # Batch in calldata
+          },
+          {
+            'hash' => '0x' + '7' * 64,
+            'transactionIndex' => '0x1',
+            'to' => '0x' + '8' * 40,
+            'input' => batch_payload.to_hex  # Same batch in calldata again
+          }
+        ]
+      end
+      
+      let(:receipts) do
+        [
+          {
+            'transactionHash' => transactions[0]['hash'],
+            'status' => '0x1',
+            'logs' => []
+          },
+          {
+            'transactionHash' => transactions[1]['hash'],
+            'status' => '0x1',
+            'logs' => []
+          }
+        ]
+      end
+      
+      it 'deduplicates by content hash, keeping earliest' do
+        result = collector.call
+        
+        expect(result.batches.length).to eq(1)
+        
+        # Should keep the one from tx index 0 (first occurrence)
+        batch = result.batches.first
+        expect(batch.l1_tx_index).to eq(0)
+        expect(batch.source).to eq(FacetBatchConstants::Source::CALLDATA)
+        
+        expect(result.stats[:deduped_batches]).to eq(1)
+      end
+    end
+    
+    context 'with mixed V1 and batch transactions' do
+      let(:batch_payload) { create_batch_payload }
+      let(:v1_payload) { create_v1_tx_payload }
+      
+      let(:transactions) do
+        [
+          {
+            'hash' => '0x' + 'a' * 64,
+            'transactionIndex' => '0x0',
+            'to' => EthTransaction::FACET_INBOX_ADDRESS.to_hex,
+            'input' => v1_payload  # V1 transaction
+          },
+          {
+            'hash' => '0x' + 'b' * 64,
+            'transactionIndex' => '0x1',
+            'to' => '0x' + 'c' * 40,
+            'input' => batch_payload.to_hex  # Batch
+          }
+        ]
+      end
+      
+      let(:receipts) do
+        transactions.map do |tx|
+          {
+            'transactionHash' => tx['hash'],
+            'status' => '0x1',
+            'logs' => []
+          }
+        end
+      end
+      
+      it 'collects both V1 and batch transactions' do
+        result = collector.call
+        
+        expect(result.single_txs.length).to eq(1)
+        expect(result.batches.length).to eq(1)
+        
+        expect(result.stats[:single_txs_calldata]).to eq(1)
+        expect(result.stats[:batches_calldata]).to eq(1)
+      end
+    end
+  end
+  
+  private
+  
+  def create_v1_tx_payload
+    # Create a valid V1 Facet transaction payload
+    tx_type = [FacetTransaction::FACET_TX_TYPE].pack('C')
+    rlp_data = Eth::Rlp.encode(['', '', '', '', '', ''])
+    '0x' + (tx_type + rlp_data).unpack1('H*')
+  end
+  
+  def create_batch_payload
+    # Create a valid batch in new wire format
+    chain_id = ChainIdManager.current_l2_chain_id
+
+    # Create empty transaction list
+    rlp_tx_list = Eth::Rlp.encode([])
+
+    # Construct wire format: [MAGIC:#{FacetBatchConstants::MAGIC_SIZE}][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST]
+    magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+    chain_id_bytes = [chain_id].pack('Q>')  # uint64 big-endian
+    version_byte = [FacetBatchConstants::VERSION].pack('C')  # uint8
+    role_byte = [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')  # uint8
+    length_bytes = [rlp_tx_list.length].pack('N')  # uint32 big-endian
+
+    ByteString.from_bin(magic + chain_id_bytes + version_byte + role_byte + length_bytes + rlp_tx_list)
+  end
+end
diff --git a/spec/services/facet_batch_parser_spec.rb b/spec/services/facet_batch_parser_spec.rb
new file mode 100644
index 00000000..12bf08df
--- /dev/null
+++ b/spec/services/facet_batch_parser_spec.rb
@@ -0,0 +1,271 @@
+require 'rails_helper'
+
+RSpec.describe FacetBatchParser do
+  let(:chain_id) { ChainIdManager.current_l2_chain_id }
+  let(:parser) { described_class.new(chain_id: chain_id) }
+  let(:l1_block_number) { 12345 }
+  let(:l1_tx_index) { 5 }
+  
+  describe '#parse_payload' do
+    context 'with valid permissionless batch' do
+      let(:rlp_tx_list) do
+        # Empty transaction list for testing
+        Eth::Rlp.encode([])
+      end
+
+      let(:payload) do
+        # Construct wire format: [MAGIC:#{FacetBatchConstants::MAGIC_SIZE}][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST]
+        magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+        chain_id_bytes = [chain_id].pack('Q>')  # uint64 big-endian
+        version_byte = [FacetBatchConstants::VERSION].pack('C')  # uint8
+        role_byte = [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')  # uint8
+        length_bytes = [rlp_tx_list.length].pack('N')  # uint32 big-endian
+
+        ByteString.from_bin(magic + chain_id_bytes + version_byte + role_byte + length_bytes + rlp_tx_list)
+      end
+
+      it 'parses a valid batch' do
+        batches = parser.parse_payload(payload, l1_tx_index, FacetBatchConstants::Source::CALLDATA)
+
+        expect(batches.length).to eq(1)
+        batch = batches.first
+
+        expect(batch.role).to eq(FacetBatchConstants::Role::PERMISSIONLESS)
+        expect(batch.l1_tx_index).to eq(l1_tx_index)
+        expect(batch.chain_id).to eq(chain_id)
+        expect(batch.transactions).to be_empty
+        expect(batch.signer).to be_nil
+      end
+    end
+    
+    context 'with invalid version' do
+      let(:rlp_tx_list) do
+        Eth::Rlp.encode([])
+      end
+
+      let(:payload) do
+        magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+        chain_id_bytes = [chain_id].pack('Q>')
+        version_byte = [2].pack('C')  # Wrong version
+        role_byte = [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+        length_bytes = [rlp_tx_list.length].pack('N')
+
+        ByteString.from_bin(magic + chain_id_bytes + version_byte + role_byte + length_bytes + rlp_tx_list)
+      end
+
+      it 'rejects batch with wrong version' do
+        batches = parser.parse_payload(payload, l1_tx_index, FacetBatchConstants::Source::CALLDATA)
+        expect(batches).to be_empty
+      end
+    end
+    
+    context 'with wrong chain ID' do
+      let(:rlp_tx_list) do
+        Eth::Rlp.encode([])
+      end
+
+      let(:payload) do
+        magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+        chain_id_bytes = [999999].pack('Q>')  # Wrong chain ID
+        version_byte = [FacetBatchConstants::VERSION].pack('C')
+        role_byte = [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+        length_bytes = [rlp_tx_list.length].pack('N')
+
+        ByteString.from_bin(magic + chain_id_bytes + version_byte + role_byte + length_bytes + rlp_tx_list)
+      end
+
+      it 'skips batch with wrong chain ID without parsing RLP' do
+        batches = parser.parse_payload(payload, l1_tx_index, FacetBatchConstants::Source::CALLDATA)
+        expect(batches).to be_empty
+      end
+    end
+    
+    context 'with multiple batches in payload' do
+      let(:rlp_tx_list) { Eth::Rlp.encode([]) }
+
+      let(:batch1) do
+        create_valid_wire_batch(chain_id, FacetBatchConstants::Role::PERMISSIONLESS, rlp_tx_list)
+      end
+
+      let(:batch2) do
+        create_valid_wire_batch(chain_id, FacetBatchConstants::Role::PERMISSIONLESS, rlp_tx_list)
+      end
+
+      let(:payload) do
+        # Add some padding between batches
+        ByteString.from_bin(batch1 + "\x00" * 10 + batch2)
+      end
+
+      it 'finds multiple batches' do
+        batches = parser.parse_payload(payload, l1_tx_index, FacetBatchConstants::Source::CALLDATA)
+        expect(batches.length).to eq(2)
+      end
+    end
+    
+    context 'with batch exceeding max size' do
+      let(:oversized_rlp) { Eth::Rlp.encode(["\x00" * (FacetBatchConstants::MAX_BATCH_BYTES + 1)]) }
+
+      let(:payload) do
+        magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+        chain_id_bytes = [chain_id].pack('Q>')
+        version_byte = [FacetBatchConstants::VERSION].pack('C')
+        role_byte = [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+        length_bytes = [oversized_rlp.length].pack('N')
+
+        ByteString.from_bin(magic + chain_id_bytes + version_byte + role_byte + length_bytes + oversized_rlp)
+      end
+
+      it 'rejects oversized batch' do
+        batches = parser.parse_payload(payload, l1_tx_index, FacetBatchConstants::Source::CALLDATA)
+        expect(batches).to be_empty
+      end
+    end
+
+    context 'with nested transaction entry' do
+      let(:malformed_rlp) { Eth::Rlp.encode([["nested"]]) }
+
+      let(:payload) do
+        magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+        chain_id_bytes = [chain_id].pack('Q>')
+        version_byte = [FacetBatchConstants::VERSION].pack('C')
+        role_byte = [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+        length_bytes = [malformed_rlp.length].pack('N')
+
+        ByteString.from_bin(magic + chain_id_bytes + version_byte + role_byte + length_bytes + malformed_rlp)
+      end
+
+      it 'rejects transaction lists with non byte-string entries' do
+        batches = parser.parse_payload(payload, l1_tx_index, FacetBatchConstants::Source::CALLDATA)
+        expect(batches).to be_empty
+      end
+    end
+
+    context 'with malformed length field' do
+      let(:payload) do
+        magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+        length = [999999].pack('N')  # Claims huge size but doesn't have the data
+        ByteString.from_bin(magic + length + "\x00" * 100)
+      end
+      
+      it 'handles malformed length gracefully' do
+        batches = parser.parse_payload(payload, l1_tx_index, FacetBatchConstants::Source::CALLDATA)
+        expect(batches).to be_empty
+      end
+    end
+  end
+  
+  private
+  
+  def create_valid_wire_batch(chain_id, role, rlp_tx_list, signature = nil)
+    # Create valid wire format batch
+    magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+    chain_id_bytes = [chain_id].pack('Q>')  # uint64 big-endian
+    version_byte = [FacetBatchConstants::VERSION].pack('C')  # uint8
+    role_byte = [role].pack('C')  # uint8
+    length_bytes = [rlp_tx_list.length].pack('N')  # uint32 big-endian
+
+    batch = magic + chain_id_bytes + version_byte + role_byte + length_bytes + rlp_tx_list
+
+    # Add signature for priority batches
+    if role == FacetBatchConstants::Role::PRIORITY && signature
+      batch += signature
+    end
+
+    batch
+  end
+
+  describe 'real blob parsing' do
+    it 'parses batch with real transaction in new format' do
+      # Create a real EIP-1559 transaction (from the old format test)
+      # This is the same transaction that was in the old blob, now in new format
+      tx_hex = '0x02f87683face7b8084773594008504a817c8008252089470997970c51812dc3a010c7d01b50e0d17dc79c888016345785d8a000080c080a09319812cf80571eaf0ff69a17e27537b4faf857c4268717ada7c2645fb0efab6a077e333b17b54b397972c1920bb1088d4de3c6a705061988a35d331d6e4c2ab60'
+
+      # Create RLP-encoded transaction list
+      tx_bytes = ByteString.from_hex(tx_hex).to_bin
+      rlp_tx_list = Eth::Rlp.encode([tx_bytes])
+
+      # Build wire format batch for chain_id 0xface7b (16436859)
+      chain_id = 0xface7b
+
+      # Construct wire format: [MAGIC:#{FacetBatchConstants::MAGIC_SIZE}][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST]
+      magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+      chain_id_bytes = [chain_id].pack('Q>')  # uint64 big-endian
+      version_byte = [FacetBatchConstants::VERSION].pack('C')
+      role_byte = [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+      length_bytes = [rlp_tx_list.length].pack('N')  # uint32 big-endian
+
+      wire_batch = magic + chain_id_bytes + version_byte + role_byte + length_bytes + rlp_tx_list
+
+      parser = described_class.new(chain_id: chain_id)
+
+      # Parse the batch
+      batches = parser.parse_payload(
+        ByteString.from_bin(wire_batch),
+        0,
+        FacetBatchConstants::Source::BLOB,
+        {}
+      )
+
+      expect(batches).not_to be_empty
+      expect(batches.length).to eq(1)
+
+      batch = batches.first
+      expect(batch.role).to eq(FacetBatchConstants::Role::PERMISSIONLESS)
+      expect(batch.transactions).to be_an(Array)
+      expect(batch.transactions.length).to eq(1)
+
+      # The transaction should be an EIP-1559 transaction
+      tx = batch.transactions.first
+      expect(tx).to be_a(ByteString)
+      
+      # Verify it can be decoded as an Ethereum transaction
+      decoded_tx = Eth::Tx.decode(tx.to_hex)
+      expect(decoded_tx).to be_a(Eth::Tx::Eip1559)
+      expect(decoded_tx.chain_id).to eq(0xface7b)
+    end
+
+    it 'parses priority batch with signature' do
+      # Create a simple transaction
+      tx_hex = '0x02f87683face7b8084773594008504a817c8008252089470997970c51812dc3a010c7d01b50e0d17dc79c888016345785d8a000080c080a09319812cf80571eaf0ff69a17e27537b4faf857c4268717ada7c2645fb0efab6a077e333b17b54b397972c1920bb1088d4de3c6a705061988a35d331d6e4c2ab60'
+
+      # Create RLP-encoded transaction list
+      tx_bytes = ByteString.from_hex(tx_hex).to_bin
+      rlp_tx_list = Eth::Rlp.encode([tx_bytes])
+
+      chain_id = 0xface7b
+
+      # Construct wire format for priority batch: [MAGIC:#{FacetBatchConstants::MAGIC_SIZE}][CHAIN_ID:8][VERSION:1][ROLE:1][LENGTH:4][RLP_TX_LIST][SIGNATURE:65]
+      magic = FacetBatchConstants::MAGIC_PREFIX.to_bin
+      chain_id_bytes = [chain_id].pack('Q>')
+      version_byte = [FacetBatchConstants::VERSION].pack('C')
+      role_byte = [FacetBatchConstants::Role::PRIORITY].pack('C')
+      length_bytes = [rlp_tx_list.length].pack('N')
+
+      # Create a dummy 65-byte signature (r: 32, s: 32, v: 1)
+      signature = "\x00" * 32 + "\x00" * 32 + "\x1b"  # v=27 (0x1b)
+
+      wire_batch = magic + chain_id_bytes + version_byte + role_byte + length_bytes + rlp_tx_list + signature
+
+      parser = described_class.new(chain_id: chain_id)
+
+      # Disable signature verification for this test
+      allow(SysConfig).to receive(:enable_sig_verify?).and_return(false)
+
+      # Parse the batch
+      batches = parser.parse_payload(
+        ByteString.from_bin(wire_batch),
+        0,
+        FacetBatchConstants::Source::BLOB,
+        {}
+      )
+
+      expect(batches).not_to be_empty
+      expect(batches.length).to eq(1)
+
+      batch = batches.first
+      expect(batch.role).to eq(FacetBatchConstants::Role::PRIORITY)
+      expect(batch.transactions.length).to eq(1)
+      expect(batch.signer).to be_nil  # Since we disabled verification
+    end
+  end
+end
diff --git a/spec/services/facet_block_builder_spec.rb b/spec/services/facet_block_builder_spec.rb
new file mode 100644
index 00000000..19179e2f
--- /dev/null
+++ b/spec/services/facet_block_builder_spec.rb
@@ -0,0 +1,263 @@
+require 'rails_helper'
+
+RSpec.describe FacetBlockBuilder do
+  let(:l1_block_number) { 12345 }
+  let(:l2_block_gas_limit) { 10_000_000 }
+  let(:authorized_signer) { Address20.from_hex('0x' + 'a' * 40) }
+  
+  let(:collected) do
+    FacetBatchCollector::CollectorResult.new(
+      single_txs: single_txs,
+      batches: batches,
+      stats: {}
+    )
+  end
+  
+  let(:single_txs) { [] }
+  let(:batches) { [] }
+  
+  let(:builder) do
+    described_class.new(
+      collected: collected,
+      l2_block_gas_limit: l2_block_gas_limit,
+      get_authorized_signer: ->(block) { authorized_signer }
+    )
+  end
+  
+  before do
+    allow(SysConfig).to receive(:enable_sig_verify?).and_return(false)
+  end
+  
+  describe '#ordered_transactions' do
+    context 'with no transactions' do
+      it 'returns empty array' do
+        transactions = builder.ordered_transactions(l1_block_number)
+        expect(transactions).to be_empty
+      end
+    end
+    
+    context 'with only V1 single transactions' do
+      let(:single_txs) do
+        [
+          create_single_tx(l1_tx_index: 2),
+          create_single_tx(l1_tx_index: 0),
+          create_single_tx(l1_tx_index: 1)
+        ]
+      end
+      
+      it 'orders by L1 transaction index' do
+        transactions = builder.ordered_transactions(l1_block_number)
+        
+        expect(transactions.length).to eq(3)
+        # Should be ordered by l1_tx_index: 0, 1, 2
+        # (actual transaction parsing would determine this)
+      end
+    end
+    
+    context 'with forced batches' do
+      let(:batches) do
+        [
+          create_forced_batch(l1_tx_index: 1, tx_count: 2),
+          create_forced_batch(l1_tx_index: 0, tx_count: 3)
+        ]
+      end
+      
+      it 'unwraps transactions in order' do
+        transactions = builder.ordered_transactions(l1_block_number)
+        
+        # Should have 5 total transactions (3 + 2)
+        expect(transactions.length).to eq(5)
+      end
+    end
+    
+    context 'with priority batch under gas limit' do
+      let(:batches) do
+        [
+          create_priority_batch(l1_tx_index: 0, tx_count: 2, signer: authorized_signer),
+          create_forced_batch(l1_tx_index: 1, tx_count: 1)
+        ]
+      end
+      
+      it 'includes priority batch first' do
+        transactions = builder.ordered_transactions(l1_block_number)
+        
+        # Priority batch (2 txs) + forced batch (1 tx)
+        expect(transactions.length).to eq(3)
+        # First 2 should be from priority batch
+      end
+    end
+    
+    context 'with priority batch over gas limit' do
+      let(:batches) do
+        [
+          create_priority_batch(
+            l1_tx_index: 0,
+            tx_count: 1000,  # Way too many transactions
+            signer: authorized_signer
+          ),
+          create_forced_batch(l1_tx_index: 1, tx_count: 1)
+        ]
+      end
+      
+      before do
+        # Mock gas calculation to exceed limit for priority batch only
+        allow_any_instance_of(described_class).to receive(:calculate_batch_gas) do |instance, batch|
+          if batch.is_priority?
+            l2_block_gas_limit + 1  # Over limit
+          else
+            100  # Under limit
+          end
+        end
+      end
+      
+      it 'discards priority batch entirely' do
+        transactions = builder.ordered_transactions(l1_block_number)
+        
+        # Only forced batch included
+        expect(transactions.length).to eq(1)
+      end
+    end
+    
+    context 'with multiple priority batches' do
+      let(:other_signer) { Address20.from_hex('0x' + 'b' * 40) }
+      
+      let(:batches) do
+        [
+          create_priority_batch(l1_tx_index: 2, tx_count: 1, signer: authorized_signer),
+          create_priority_batch(l1_tx_index: 0, tx_count: 1, signer: authorized_signer),
+          create_priority_batch(l1_tx_index: 1, tx_count: 1, signer: other_signer)
+        ]
+      end
+      
+      it 'selects priority batch with lowest index from authorized signer' do
+        transactions = builder.ordered_transactions(l1_block_number)
+        
+        # Should select the one at index 0 (authorized, lowest index)
+        expect(transactions.length).to eq(1)
+      end
+    end
+    
+    context 'with signature verification enabled' do
+      before do
+        allow(SysConfig).to receive(:enable_sig_verify?).and_return(true)
+      end
+      
+      let(:batches) do
+        [
+          create_priority_batch(l1_tx_index: 0, tx_count: 1, signer: nil),  # No signature
+          create_priority_batch(l1_tx_index: 1, tx_count: 1, signer: authorized_signer)
+        ]
+      end
+      
+      it 'only accepts signed priority batches' do
+        transactions = builder.ordered_transactions(l1_block_number)
+        
+        # Should select the signed one at index 1
+        expect(transactions.length).to eq(1)
+      end
+    end
+    
+    context 'with mixed priority and forced batches' do
+      let(:batches) do
+        [
+          create_forced_batch(l1_tx_index: 0, tx_count: 2),
+          create_priority_batch(l1_tx_index: 1, tx_count: 3, signer: authorized_signer),
+          create_forced_batch(l1_tx_index: 2, tx_count: 1)
+        ]
+      end
+      
+      it 'orders priority first, then forced by index' do
+        transactions = builder.ordered_transactions(l1_block_number)
+        
+        # Priority (3) + forced at index 0 (2) + forced at index 2 (1) = 6 total
+        expect(transactions.length).to eq(6)
+      end
+    end
+  end
+  
+  private
+  
+  def create_single_tx(l1_tx_index:)
+    {
+      source: 'calldata',
+      l1_tx_index: l1_tx_index,
+      tx_hash: '0x' + rand(16**64).to_s(16).rjust(64, '0'),
+      payload: create_v1_payload,
+      events: []
+    }
+  end
+  
+  def create_forced_batch(l1_tx_index:, tx_count:)
+    transactions = tx_count.times.map { create_tx_bytes }
+
+    ParsedBatch.new(
+      role: FacetBatchConstants::Role::PERMISSIONLESS,
+      signer: nil,
+      l1_tx_index: l1_tx_index,
+      source: FacetBatchConstants::Source::CALLDATA,
+      source_details: {},
+      transactions: transactions,
+      content_hash: Hash32.from_bin(Eth::Util.keccak256(rand.to_s)),
+      chain_id: ChainIdManager.current_l2_chain_id
+    )
+  end
+
+  def create_priority_batch(l1_tx_index:, tx_count:, signer:)
+    transactions = tx_count.times.map { create_tx_bytes }
+
+    ParsedBatch.new(
+      role: FacetBatchConstants::Role::PRIORITY,
+      signer: signer,
+      l1_tx_index: l1_tx_index,
+      source: FacetBatchConstants::Source::CALLDATA,
+      source_details: {},
+      transactions: transactions,
+      content_hash: Hash32.from_bin(Eth::Util.keccak256(rand.to_s)),
+      chain_id: ChainIdManager.current_l2_chain_id
+    )
+  end
+  
+  def create_v1_payload
+    tx_type = [FacetTransaction::FACET_TX_TYPE].pack('C')
+    chain_id = Eth::Util.serialize_int_to_big_endian(ChainIdManager.current_l2_chain_id)
+    rlp_data = Eth::Rlp.encode([chain_id, '', '', '', '', ''])
+    ByteString.from_bin(tx_type + rlp_data)
+  end
+  
+  def create_tx_bytes
+    # Create a simple EIP-1559 transaction for testing with valid signature
+    # This is what would be in batches - standard Ethereum transactions
+    
+    chain_id = ChainIdManager.current_l2_chain_id
+    
+    # Transaction data (without signature)
+    tx_data_unsigned = [
+      Eth::Util.serialize_int_to_big_endian(chain_id),
+      Eth::Util.serialize_int_to_big_endian(0),  # nonce
+      Eth::Util.serialize_int_to_big_endian(1_000_000_000),  # max_priority_fee (1 gwei)
+      Eth::Util.serialize_int_to_big_endian(2_000_000_000),  # max_fee (2 gwei)
+      Eth::Util.serialize_int_to_big_endian(21_000),  # gas_limit
+      "\x11" * 20,  # to address (20 bytes)
+      Eth::Util.serialize_int_to_big_endian(0),  # value
+      '',  # data
+      []  # access_list
+    ]
+    
+    # For testing, use valid but dummy signature values
+    # Real signatures would be created by wallet software
+    # Using non-zero values to avoid Geth rejection
+    r = "\x00" * 31 + "\x01"  # 32 bytes, non-zero
+    s = "\x00" * 31 + "\x02"  # 32 bytes, non-zero
+    
+    # Build complete transaction with signature
+    # For EIP-1559, v should be 0 or 1
+    tx_data = tx_data_unsigned + [
+      Eth::Util.serialize_int_to_big_endian(0),  # v (0 for EIP-1559)
+      r,  # r (32 bytes)
+      s   # s (32 bytes)
+    ]
+    
+    # Prefix with transaction type 0x02 for EIP-1559
+    ByteString.from_bin("\x02" + Eth::Rlp.encode(tx_data))
+  end
+end
\ No newline at end of file
diff --git a/spec/services/fct_mint_calculator_spec.rb b/spec/services/fct_mint_calculator_spec.rb
index f247ca31..8aa5a89d 100644
--- a/spec/services/fct_mint_calculator_spec.rb
+++ b/spec/services/fct_mint_calculator_spec.rb
@@ -21,8 +21,13 @@
   end
 
   def build_tx(burn_tokens)
-    tx = OpenStruct.new
-    tx.define_singleton_method(:l1_data_gas_used) { |_blk_num| burn_tokens }
+    # Create a mock FacetTransaction that responds to the necessary methods
+    tx = instance_double(FacetTransaction)
+    mint_value = nil
+    allow(tx).to receive(:is_a?).with(FacetTransaction).and_return(true)
+    allow(tx).to receive(:l1_data_gas_used).and_return(burn_tokens)
+    allow(tx).to receive(:mint=) { |val| mint_value = val }
+    allow(tx).to receive(:mint) { mint_value }
     tx
   end
   
diff --git a/spec/support/blob_test_helper.rb b/spec/support/blob_test_helper.rb
new file mode 100644
index 00000000..c249ef0d
--- /dev/null
+++ b/spec/support/blob_test_helper.rb
@@ -0,0 +1,188 @@
+# Helper methods for testing blob functionality
+require 'blob_utils'
+
+module BlobTestHelper
+  # Create a test blob with Facet batch data embedded using proper EIP-4844 encoding
+  def create_test_blob_with_facet_data(transactions: [], position: :start)
+    # Create RLP transaction list
+    rlp_tx_list = create_test_batch_data(transactions)
+
+    # Build complete wire format
+    chain_id = ChainIdManager.current_l2_chain_id
+    facet_payload = FacetBatchConstants::MAGIC_PREFIX.to_bin
+    facet_payload += [chain_id].pack('Q>')  # uint64 big-endian
+    facet_payload += [FacetBatchConstants::VERSION].pack('C')
+    facet_payload += [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+    facet_payload += [rlp_tx_list.length].pack('N')
+    facet_payload += rlp_tx_list
+    
+    # Create aggregated data based on position
+    aggregated_data = case position
+    when :start
+      # Facet data at beginning
+      facet_payload + ("\x00".b * 1000)  # Some padding
+    when :middle
+      # Facet data in middle (simulating aggregation with other users)
+      padding_before = "\xFF".b * 5_000  # Other user's data
+      padding_after = "\xEE".b * 2_000
+      padding_before + facet_payload + padding_after
+    when :end
+      # Facet data at end
+      padding = "\xAB".b * 10_000
+      padding + facet_payload
+    when :multiple
+      # Multiple Facet batches in same blob
+      second_rlp_tx_list = create_test_batch_data([create_test_transaction])
+
+      # Build complete wire format for second batch
+      second_payload = FacetBatchConstants::MAGIC_PREFIX.to_bin
+      second_payload += [chain_id].pack('Q>')  # uint64 big-endian
+      second_payload += [FacetBatchConstants::VERSION].pack('C')
+      second_payload += [FacetBatchConstants::Role::PERMISSIONLESS].pack('C')
+      second_payload += [second_rlp_tx_list.length].pack('N')
+      second_payload += second_rlp_tx_list
+      
+      # Put both batches with padding between
+      first_part = facet_payload
+      padding = "\xCD".b * 1_000
+      second_part = second_payload
+      
+      first_part + padding + second_part
+    else
+      raise "Unknown position: #{position}"
+    end
+    
+    # Use BlobUtils to properly encode into EIP-4844 blob format
+    blobs = BlobUtils.to_blobs(data: aggregated_data)
+    
+    # Return the first blob as ByteString (should only need one for our test data)
+    ByteString.from_hex(blobs.first)
+  end
+  
+  # Create test batch data (RLP-encoded transaction list)
+  def create_test_batch_data(transactions = [])
+    # Default to one test transaction if none provided
+    if transactions.empty?
+      transactions = [create_test_transaction]
+    end
+
+    # Return RLP-encoded transaction list
+    Eth::Rlp.encode(transactions.map(&:to_bin))
+  end
+  
+  # Create a test EIP-1559 transaction
+  def create_test_transaction(to: nil, value: 0, nonce: 0)
+    to_address = to || ("0x" + "1" * 40)
+    
+    # Create minimal EIP-1559 transaction
+    tx_data = [
+      Eth::Util.serialize_int_to_big_endian(ChainIdManager.current_l2_chain_id),
+      Eth::Util.serialize_int_to_big_endian(nonce),
+      Eth::Util.serialize_int_to_big_endian(1_000_000_000),  # maxPriorityFee
+      Eth::Util.serialize_int_to_big_endian(2_000_000_000),  # maxFee
+      Eth::Util.serialize_int_to_big_endian(21_000),  # gasLimit
+      Eth::Util.hex_to_bin(to_address),  # to
+      Eth::Util.serialize_int_to_big_endian(value),  # value
+      '',  # data
+      [],  # accessList
+      Eth::Util.serialize_int_to_big_endian(0),  # v
+      "\x00".b * 31 + "\x01".b,  # r (dummy)
+      "\x00".b * 31 + "\x02".b   # s (dummy)
+    ]
+    
+    # Prefix with type byte for EIP-1559
+    ByteString.from_bin("\x02".b + Eth::Rlp.encode(tx_data))
+  end
+  
+  # Stub beacon client responses
+  def stub_beacon_blob_response(versioned_hash, blob_data)
+    beacon_provider = instance_double(BlobProvider)
+    
+    allow(beacon_provider).to receive(:get_blob).with(versioned_hash, anything) do
+      blob_data
+    end
+    
+    allow(beacon_provider).to receive(:list_carriers).and_return([
+      {
+        tx_hash: "0x" + "a" * 64,
+        tx_index: 0,
+        versioned_hashes: [versioned_hash]
+      }
+    ])
+    
+    beacon_provider
+  end
+  
+  # Create a mock beacon API response
+  def create_beacon_blob_sidecar_response(blob_data, slot: 1000, index: 0)
+    # Ensure blob is properly sized
+    blob_bytes = blob_data.to_bin
+    if blob_bytes.length != BlobUtils::BYTES_PER_BLOB
+      # Encode to proper blob if not already
+      blobs = BlobUtils.to_blobs(data: blob_bytes)
+      blob_bytes = [blobs.first.sub(/^0x/, '')].pack('H*')
+    end
+    
+    # Beacon API blob sidecar format
+    {
+      "index" => index.to_s,
+      "blob" => Base64.encode64(blob_bytes),
+      "kzg_commitment" => "0x" + "b" * 96,  # Dummy KZG commitment
+      "kzg_proof" => "0x" + "c" * 96,  # Dummy KZG proof
+      "signed_block_header" => {
+        "message" => {
+          "slot" => slot.to_s,
+          "proposer_index" => "12345",
+          "parent_root" => "0x" + "d" * 64,
+          "state_root" => "0x" + "e" * 64,
+          "body_root" => "0x" + "f" * 64
+        }
+      },
+      "kzg_commitment_inclusion_proof" => ["0x" + "0" * 64] * 17
+    }
+  end
+  
+  # Simulate blob aggregation scenario (multiple rollups in one blob)
+  def create_aggregated_blob(rollup_payloads)
+    # Simulate how DA Builder would aggregate multiple rollups
+    combined_data = ""
+    
+    rollup_payloads.each_with_index do |payload, i|
+      # Add some padding between payloads to simulate real aggregation
+      combined_data += ("\xEE".b * rand(100..500)) if i > 0
+      combined_data += payload.is_a?(ByteString) ? payload.to_bin : payload
+    end
+    
+    # Use BlobUtils to create proper EIP-4844 blob
+    blobs = BlobUtils.to_blobs(data: combined_data)
+    ByteString.from_hex(blobs.first)
+  end
+  
+  # Helper to verify batch extraction from blob
+  def extract_facet_batches_from_blob(blob_data)
+    # First decode from EIP-4844 blob format if it's a properly encoded blob
+    decoded_data = if blob_data.to_bin.length == BlobUtils::BYTES_PER_BLOB
+      # This is a full blob, decode it
+      BlobUtils.from_blobs(blobs: [blob_data.to_hex])
+    else
+      # Raw data, use as-is
+      blob_data
+    end
+    
+    parser = FacetBatchParser.new
+    parser.parse_payload(
+      decoded_data.is_a?(String) ? ByteString.from_hex(decoded_data) : decoded_data,
+      0,      # l1_tx_index
+      FacetBatchConstants::Source::BLOB,
+      { versioned_hash: "0x" + "a" * 64 }
+    )
+  end
+  
+  # Create test blob commitment (not cryptographically valid)
+  def create_test_blob_commitment(blob_data)
+    # WARNING: This is NOT a real KZG commitment
+    # For testing only - real implementation needs ckzg library
+    hash = Eth::Util.keccak256(blob_data.to_bin)
+    "0x01" + hash[2..63]  # Version prefix + truncated hash
+  end
+end
\ No newline at end of file
diff --git a/spec/support/facet_transaction_helper.rb b/spec/support/facet_transaction_helper.rb
index 1f818349..8d39fb66 100644
--- a/spec/support/facet_transaction_helper.rb
+++ b/spec/support/facet_transaction_helper.rb
@@ -5,6 +5,15 @@ def import_eth_txs(transactions)
     
     # Convert transaction params to EthTransaction objects
     eth_transactions = transactions.map.with_index do |tx_params, index|
+      to_address =
+        if tx_params[:to_address]
+          tx_params[:to_address].is_a?(Address20) ? tx_params[:to_address] : Address20.from_hex(tx_params[:to_address])
+        elsif tx_params[:events].present?
+          Address20.from_hex("0x" + "2" * 40)
+        else
+          EthTransaction::FACET_INBOX_ADDRESS
+        end
+      
       EthTransaction.new(
         block_hash: Hash32.from_hex(bytes_stub(rand)),
         block_number: current_max_eth_block.number + 1,
@@ -14,12 +23,12 @@ def import_eth_txs(transactions)
         input: ByteString.from_hex(tx_params[:input]),
         chain_id: 1,
         from_address: Address20.from_hex(tx_params[:from_address] || "0x" + "2" * 40),
-        to_address: EthTransaction::FACET_INBOX_ADDRESS,
+        to_address: to_address,
         status: 1,
         logs: tx_params[:events] || []
       )
     end
-
+    
     rpc_results = eth_txs_to_rpc_result(eth_transactions)
     block_result = rpc_results[0].merge('parentHash' => current_max_eth_block.block_hash.to_hex)
     receipt_result = rpc_results[1]
@@ -27,13 +36,21 @@ def import_eth_txs(transactions)
     # Create the mock response for the prefetcher
     eth_block = EthBlock.from_rpc_result(block_result)
     facet_block = FacetBlock.from_eth_block(eth_block)
-    facet_txs = EthTransaction.facet_txs_from_rpc_results(block_result, receipt_result)
+
+    # Use batch collection v2 if enabled, otherwise use v1
+    facet_txs = if SysConfig.facet_batch_v2_enabled?
+      L1RpcPrefetcher.send(:allocate).collect_facet_transactions_v2(block_result, receipt_result)
+    else
+      EthTransaction.facet_txs_from_rpc_results(block_result, receipt_result)
+    end
 
     mock_prefetcher_response = {
       error: nil,
       eth_block: eth_block,
       facet_block: facet_block,
-      facet_txs: facet_txs
+      facet_txs: facet_txs,
+      block_result: block_result,
+      receipt_result: receipt_result
     }
 
     # Mock the prefetcher
@@ -58,8 +75,12 @@ def import_eth_txs(transactions)
     latest_l2_block = EthRpcClient.l2.get_block("latest", true)
     # binding.irb
     # Return array of receipts
+    # NOTE: This only returns receipts for V1 single transactions that have a direct sourceHash mapping
+    # Batch transactions (StandardL2Transaction) don't have a 1:1 mapping with L1 transactions
+    # and should be queried directly from the L2 block if needed
     res = eth_transactions.map do |eth_tx|
       tx_in_geth = latest_l2_block['transactions'].find do |tx|
+        next false if tx['sourceHash'].nil?
         eth_tx.facet_tx_source_hash == Hash32.from_hex(tx['sourceHash'])
       end