Repository: moondance-labs/tanssi-symbiotic
Pull request #75: Added chainlink monitor script with slack integration
Author: login: NindoK
Labels: name: D3-trivial
description: PR contains trivial changes in a runtime directory that do not require an audit
name: not-breaking
description: Does not need to be mentioned in breaking changes
Created: 2025-04-16T03:48:57Z | Merged: 2025-04-29T10:09:10Z

This PR adds script to monitor on chain event every time operators keys are sent to the gateway. 

It can:
- by passing a lookback period, it will fetch all the past events and check if there are any mismatch
- listen to all the new events emitted and compare the payloads being sent.

In a case of mismatch a message to slack is sent

Example of slack message:
<img width="693" alt="Screenshot 2025-04-16 at 12 54 13" src="https://github.com/user-attachments/assets/4ffd343c-48b9-4f58-8374-977532f6798c" />

If the `sendCurrentOperatorsKeys` is called then another message will be shown
<img width="684" alt="Screenshot 2025-04-17 at 01 09 06" src="https://github.com/user-attachments/assets/1e8769ec-a405-462b-b079-29950f4588b2" />



Diff:
diff --git a/.github/workflows/demo-test.yml b/.github/workflows/demo-test.yml
index e115313a..4e8570c4 100644
--- a/.github/workflows/demo-test.yml
+++ b/.github/workflows/demo-test.yml
@@ -1,12 +1,32 @@
 name: Deploy Tanssi Ecosystem Demo
 
 on:
+  push:
+    branches:
+      - staging
+      - main
   pull_request:
   workflow_dispatch:
 
 jobs:
+  detect-changes:
+    runs-on: ubuntu-latest
+    outputs:
+      solidity_changed: ${{ steps.filter.outputs.solidity }}
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dorny/paths-filter@v3
+        id: filter
+        with:
+          filters: |
+            solidity:
+              - '**/*.sol' # Match any file ending in .sol anywhere in the repo
+
   deploy:
     runs-on: ubuntu-latest
+    needs: [detect-changes]
+    # Only run this job if the 'solidity' filter from detect-changes job is true
+    if: needs.detect-changes.outputs.solidity_changed == 'true'
     steps:
       - name: Checkout code
         uses: actions/checkout@v4
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 1158a2a0..e1fc808d 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -2,9 +2,11 @@ name: Test
 
 on:
   push:
+    branches:
+      - staging
+      - main
   pull_request:
   workflow_dispatch:
-
 env:
   FOUNDRY_PROFILE: ci
 
@@ -37,6 +39,19 @@ jobs:
             echo "coverage_dir=tanssi-symbiotic-coverage/branches/master" >> $GITHUB_OUTPUT
             echo "coverage_report=false" >> $GITHUB_OUTPUT
           fi
+  detect-changes:
+    runs-on: ubuntu-latest
+    outputs:
+      solidity_changed: ${{ steps.filter.outputs.solidity }}
+    steps:
+      - uses: actions/checkout@v4
+      - uses: dorny/paths-filter@v3
+        id: filter
+        with:
+          filters: |
+            solidity:
+              - '**/*.sol' # Match any file ending in .sol anywhere in the repo
+
   check:
     strategy:
       fail-fast: true
@@ -52,7 +67,9 @@ jobs:
       RESOLVER_PRIVATE_KEY: "0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a"
       OPERATOR_PRIVATE_KEY: "0x7c852118294e51e653712a81e05800f419141751be58f605c371e15141b007a6"
 
-    needs: ["set-tags"]
+    needs: [set-tags, detect-changes] # Add detect-changes dependency
+    # Only run this job if the 'solidity' filter from detect-changes job is true
+    if: needs.detect-changes.outputs.solidity_changed == 'true'
     steps:
       - uses: actions/checkout@v4
         with:
diff --git a/utils/chainlink_monitor/README.md b/utils/chainlink_monitor/README.md
new file mode 100644
index 00000000..9806e417
--- /dev/null
+++ b/utils/chainlink_monitor/README.md
@@ -0,0 +1,23 @@
+# How to use
+
+## Install dependencies
+
+```bash
+pnpm i
+```
+
+## Run the script
+
+### Run the script in watch mode
+
+```bash
+pnpm start
+```
+
+### Run the script with a lookback period
+
+This will run the script with a 1 hour (3600 seconds) lookback period. You can change the number to any value you want.
+
+```bash
+pnpm start -- 3600
+```
diff --git a/utils/chainlink_monitor/chainlink_monitor_slack_alert.ts b/utils/chainlink_monitor/chainlink_monitor_slack_alert.ts
new file mode 100644
index 00000000..9d496c06
--- /dev/null
+++ b/utils/chainlink_monitor/chainlink_monitor_slack_alert.ts
@@ -0,0 +1,219 @@
+import { ethers } from "ethers";
+import * as dotenv from "dotenv";
+import { compareWithMiddlewareData } from "./compare";
+
+dotenv.config();
+
+// Environment variables
+const RPC_URL = process.env.RPC_URL;
+const GATEWAY_ADDRESS = process.env.GATEWAY_ADDRESS;
+const MIDDLEWARE_ADDRESS = process.env.MIDDLEWARE_ADDRESS;
+export const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
+export const PRIVATE_KEY = process.env.PRIVATE_KEY;
+
+// Gateway ABI - just the parts we need for the event
+const gatewayAbi = [
+  "event OperatorsDataCreated(uint256 indexed validatorsCount, bytes payload)",
+];
+
+// Middleware ABI - just the parts we need for functions
+const middlewareAbi = [
+  {
+    inputs: [{ internalType: "uint48", name: "epoch", type: "uint48" }],
+    stateMutability: "view",
+    type: "function",
+    name: "sortOperatorsByPower",
+    outputs: [
+      {
+        internalType: "bytes32[]",
+        name: "sortedKeys",
+        type: "bytes32[]",
+      },
+    ],
+  },
+  {
+    type: "function",
+    name: "getEpochAtTs",
+    inputs: [{ name: "timestamp", type: "uint48", internalType: "uint48" }],
+    outputs: [{ name: "epoch", type: "uint48", internalType: "uint48" }],
+    stateMutability: "view",
+  },
+  {
+    inputs: [],
+    stateMutability: "nonpayable",
+    type: "function",
+    name: "sendCurrentOperatorsKeys",
+    outputs: [
+      {
+        internalType: "bytes32[]",
+        name: "sortedKeys",
+        type: "bytes32[]",
+      },
+    ],
+  },
+];
+
+async function main(): Promise<void> {
+  if (
+    !RPC_URL ||
+    !GATEWAY_ADDRESS ||
+    !SLACK_WEBHOOK_URL ||
+    !MIDDLEWARE_ADDRESS
+  ) {
+    throw new Error(
+      "Missing required environment variables. Check your .env file."
+    );
+  }
+
+  // Parse command line arguments for lookback time in seconds
+  const lookbackSeconds = parseLookbackArgument();
+
+  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
+
+  // Gateway contract instance
+  const gatewayContract = new ethers.Contract(
+    GATEWAY_ADDRESS,
+    gatewayAbi,
+    provider
+  );
+
+  // Middleware contract instance
+  const middlewareContract = new ethers.Contract(
+    MIDDLEWARE_ADDRESS,
+    middlewareAbi,
+    provider
+  );
+
+  // If lookback is specified, first query historical events
+  if (lookbackSeconds > 0) {
+    await queryHistoricalEvents(
+      provider,
+      gatewayContract,
+      middlewareContract,
+      lookbackSeconds
+    );
+  }
+
+  console.log(`Monitoring events from Gateway at ${GATEWAY_ADDRESS}...`);
+
+  // Listen for OperatorsDataCreated events
+  gatewayContract.on(
+    "OperatorsDataCreated",
+    async (
+      validatorsCount: ethers.BigNumber,
+      payload: string,
+      event: ethers.Event
+    ) => {
+      const blockNumber = event.blockNumber;
+      const txHash = event.transactionHash;
+
+      console.log(`
+      Event detected at block ${blockNumber}
+      Transaction: ${txHash}
+      Validators Count: ${validatorsCount.toString()}
+      Payload: ${payload}
+    `);
+      const currentBlockData = await provider.getBlock(event.blockNumber);
+      const timestamp = currentBlockData.timestamp;
+
+      await compareWithMiddlewareData(
+        middlewareContract,
+        payload,
+        timestamp,
+        event,
+        provider,
+        true
+      );
+    }
+  );
+}
+
+// Parse command line argument for lookback time
+function parseLookbackArgument(): number {
+  const args = process.argv.slice(2);
+  if (args.length === 0) {
+    return 0; // Default: no lookback
+  }
+
+  const lookbackSeconds = parseInt(args[1], 10);
+  if (isNaN(lookbackSeconds) || lookbackSeconds < 0) {
+    console.log("Invalid lookback time. Using default (0 seconds).");
+    return 0;
+  }
+
+  console.log(`Looking back ${lookbackSeconds} seconds for missed events...`);
+  return lookbackSeconds;
+}
+
+// Query historical events based on lookback time
+async function queryHistoricalEvents(
+  provider: ethers.providers.JsonRpcProvider,
+  gatewayContract: ethers.Contract,
+  middlewareContract: ethers.Contract,
+  lookbackSeconds: number
+): Promise<void> {
+  try {
+    // Get current block
+    const currentBlock = await provider.getBlockNumber();
+
+    // Estimate starting block based on average block time (13 seconds for Ethereum)
+    const averageBlockTime = 13; // seconds
+    const blocksToLookBack = Math.ceil(lookbackSeconds / averageBlockTime);
+    const fromBlock = Math.max(0, currentBlock - blocksToLookBack);
+
+    console.log(
+      `Querying for historical events from block ${fromBlock} to ${currentBlock}...`
+    );
+
+    // Query for past events
+    const filter = gatewayContract.filters.OperatorsDataCreated();
+    const events = await gatewayContract.queryFilter(
+      filter,
+      fromBlock,
+      currentBlock
+    );
+
+    console.log(
+      `Found ${events.length} historical events, processing them now...`
+    );
+
+    // Process each event
+    for (const event of events) {
+      const currentBlockData = await provider.getBlock(event.blockNumber);
+      const timestamp = currentBlockData.timestamp;
+
+      // Extract event arguments
+      const args = event.args;
+      if (!args) continue;
+
+      const [validatorsCount, payload] = args;
+
+      // Only process events from our lookback window
+      console.log(`
+          Historical event found at block ${event.blockNumber}
+          Transaction: ${event.transactionHash}
+          Validators Count: ${validatorsCount.toString()}
+          Payload: ${payload}
+        `);
+
+      await compareWithMiddlewareData(
+        middlewareContract,
+        payload,
+        timestamp,
+        event,
+        provider,
+        false
+      );
+    }
+
+    console.log("Historical event processing complete.");
+  } catch (error) {
+    console.error("Error processing historical events:", error);
+  }
+}
+
+// Error handling for the main function
+main().catch((error) => {
+  console.error("Fatal error:", error);
+  process.exit(1);
+});
diff --git a/utils/chainlink_monitor/compare.ts b/utils/chainlink_monitor/compare.ts
new file mode 100644
index 00000000..ca4d0e63
--- /dev/null
+++ b/utils/chainlink_monitor/compare.ts
@@ -0,0 +1,154 @@
+import { ethers } from "ethers";
+import { sendSlackAlert } from "./send_slack";
+import { decodeOperatorsData } from "./decode_operators_data";
+import { PRIVATE_KEY } from "./chainlink_monitor_slack_alert";
+
+export async function compareWithMiddlewareData(
+  middlewareContract: ethers.Contract,
+  payload: string,
+  timestamp: number,
+  event: ethers.Event,
+  provider: ethers.providers.Provider,
+  shouldSendTx: boolean
+): Promise<void> {
+  const epoch = await middlewareContract.getEpochAtTs(timestamp);
+  console.log("Epoch:", epoch.toString());
+
+  try {
+    // Get sorted operators from middleware
+    const verifiedOperatorKeys = await middlewareContract.sortOperatorsByPower(
+      epoch
+    );
+
+    const {
+      oracleOperatorKeys,
+      operatorsCount,
+      epoch: epochValue,
+    } = decodeOperatorsData(payload);
+
+    // Compare the data
+    const comparisonResult = await compareOperatorsData(
+      verifiedOperatorKeys,
+      epoch,
+      oracleOperatorKeys,
+      epochValue,
+      operatorsCount
+    );
+
+    if (!comparisonResult.match && "message" in comparisonResult) {
+      console.log("Discrepancies found:", comparisonResult.message);
+
+      // Send to Slack
+      await sendSlackAlert({
+        message: comparisonResult.message,
+        blockNumber: event.blockNumber,
+        timestamp,
+        txHash: event.transactionHash,
+      });
+
+      // We only send the keys if its the latest epoch since we can't override the keys of past epochs
+      if (shouldSendTx && PRIVATE_KEY !== undefined) {
+        const wallet = new ethers.Wallet(PRIVATE_KEY, provider);
+        middlewareContract = middlewareContract.connect(wallet);
+        console.log("Sending transaction to override latest operator keys...");
+        const tx = await middlewareContract.sendCurrentOperatorsKeys();
+        const message = `Sent transaction to override latest operator keys after finding tampered data.`;
+        const receipt = await tx.wait();
+        const blockData = await provider.getBlock(receipt.blockNumber);
+        console.log("Transaction sent:", tx.hash);
+
+        await sendSlackAlert({
+          message: message,
+          blockNumber: receipt.blockNumber,
+          timestamp: blockData.timestamp,
+          txHash: tx.hash,
+          shouldSendTx,
+          header: "✅ Transaction sent to override latest operator keys",
+        });
+      }
+    }
+  } catch (error) {
+    console.error("Error comparing operator data:", error);
+  }
+}
+
+export async function compareOperatorsData(
+  sortedOperators: string[],
+  epoch: number,
+  operatorsKeysPayload: string[],
+  epochPayload: number,
+  operatorsCount: number
+): Promise<
+  | {
+      match: boolean;
+      message: string;
+    }
+  | {
+      match: boolean;
+      error?: string;
+    }
+> {
+  try {
+    let message = "";
+    if (epoch !== epochPayload) {
+      message = `Epoch mismatch: Middleware epoch ${epoch} vs Payload epoch ${epochPayload}`;
+      return { match: false, message };
+    }
+
+    // Check length match
+    if (sortedOperators.length !== operatorsKeysPayload.length) {
+      message = `Length mismatch: Middleware length ${sortedOperators.length} vs Payload length ${operatorsKeysPayload.length}`;
+      return { match: false, message };
+    }
+
+    if (
+      sortedOperators.length !== operatorsCount ||
+      operatorsKeysPayload.length !== operatorsCount ||
+      sortedOperators.length !== operatorsKeysPayload.length
+    ) {
+      message = `Operators count mismatch: Middleware count ${sortedOperators.length} vs Payload count ${operatorsKeysPayload.length} vs Operators count ${operatorsCount}`;
+      return { match: false, message };
+    }
+
+    // Check each element
+    const differences: {
+      [key: string]: {
+        middleware: string;
+        gateway: string;
+      };
+    } = {};
+
+    let hasDiscrepancy = false;
+
+    for (let i = 0; i < sortedOperators.length; i++) {
+      if (sortedOperators[i] !== operatorsKeysPayload[i]) {
+        hasDiscrepancy = true;
+        differences[`index_${i}`] = {
+          middleware: sortedOperators[i],
+          gateway: operatorsKeysPayload[i],
+        };
+      }
+    }
+    if (hasDiscrepancy) {
+      message = `:warning: *Mismatch in Operators for Epoch ${epoch}* :warning:\n\n`;
+
+      message += `The following discrepancies were found:\n\n`;
+      for (const [key, diff] of Object.entries(differences)) {
+        const index = key.replace("index_", "");
+        message += `*At position ${index}:*\n`;
+        message += `> *Middleware:* ${diff.middleware}\n`;
+        message += `> *Gateway:* ${diff.gateway}\n\n`;
+      }
+    }
+    return {
+      match: !hasDiscrepancy,
+      message,
+    };
+  } catch (error) {
+    console.error("Error decoding or comparing data:", error);
+    return {
+      match: false,
+      error: `Error decoding or comparing data: ${(error as Error).message}`,
+    };
+  }
+}
diff --git a/utils/chainlink_monitor/decode_operators_data.ts b/utils/chainlink_monitor/decode_operators_data.ts
new file mode 100644
index 00000000..9d3c6f61
--- /dev/null
+++ b/utils/chainlink_monitor/decode_operators_data.ts
@@ -0,0 +1,85 @@
+import { ethers } from "ethers";
+
+export type OperatorsData = {
+  oracleOperatorKeys: string[];
+  operatorsCount: number;
+  epoch: number;
+};
+
+export function decodeOperatorsData(payload: string): OperatorsData {
+  // Convert to bytes array if it's a hex string
+  const bytes = ethers.utils.arrayify(payload);
+
+  // Extract parts based on the encoding pattern:
+  // [0-3]: bytes4(0x70150038) - Magic bytes/selector
+  // [4]: bytes1(uint8(Message.V0)) - Message version
+  // [5]: bytes1(uint8(OutboundCommandV1.ReceiveValidators)) - Command type
+  // [6-?]: ScaleCodec.encodeCompactU32(operatorsCount) - Operators count (variable length)
+  // [?-(n*32)]: operatorsFlattened - The keys as flattened bytes
+  // [last 8 bytes]: ScaleCodec.encodeU64(uint64(epoch)) - Epoch
+
+  // First verify the magic bytes
+  const magicBytes = bytes.slice(0, 4);
+  const expectedMagic = [0x70, 0x15, 0x00, 0x38];
+  if (!magicBytes.every((b, i) => b === expectedMagic[i])) {
+    throw new Error("Invalid magic bytes in payload");
+  }
+
+  // Skip version and command (1 byte each)
+  let offset = 6;
+
+  // Decode the SCALE-encoded operators count
+  // Basic SCALE compact decoding for uint
+  let operatorsCount = 0;
+  const firstByte = bytes[offset];
+
+  if ((firstByte & 0b11) === 0b00) {
+    // Single-byte mode
+    operatorsCount = firstByte >> 2;
+    offset += 1;
+  } else if ((firstByte & 0b11) === 0b01) {
+    // Two-byte mode
+    operatorsCount = ((bytes[offset + 1] << 6) | (firstByte >> 2)) & 0x3fff;
+    offset += 2;
+  } else if ((firstByte & 0b11) === 0b10) {
+    // Four-byte mode
+    operatorsCount =
+      ((bytes[offset + 3] << 22) |
+        (bytes[offset + 2] << 14) |
+        (bytes[offset + 1] << 6) |
+        (firstByte >> 2)) &
+      0x3fffffff;
+    offset += 4;
+  } else {
+    throw new Error("Operators count too large");
+  }
+
+  // Extract flattened operators keys
+  const operatorsFlattened = bytes.slice(offset, offset + operatorsCount * 32);
+  offset += operatorsCount * 32;
+
+  // Last 8 bytes are the epoch
+  const epochBytes = bytes.slice(bytes.length - 8);
+
+  // SCALE codec uses little-endian, so we need to reverse the bytes for ethers.js which expects big-endian
+  const reversedEpochBytes = new Uint8Array(epochBytes).reverse();
+
+  const epochHex = ethers.utils.hexlify(reversedEpochBytes);
+
+  const epochValue = ethers.BigNumber.from(epochHex).toNumber();
+
+  // Convert flattened operators back to bytes32 array
+  const oracleOperatorKeys: string[] = [];
+  for (let i = 0; i < operatorsCount; i++) {
+    const key = ethers.utils.hexlify(
+      operatorsFlattened.slice(i * 32, (i + 1) * 32)
+    );
+    oracleOperatorKeys.push(key);
+  }
+
+  return {
+    oracleOperatorKeys,
+    operatorsCount,
+    epoch: epochValue,
+  };
+}
diff --git a/utils/chainlink_monitor/package.json b/utils/chainlink_monitor/package.json
new file mode 100644
index 00000000..7fdc1ba7
--- /dev/null
+++ b/utils/chainlink_monitor/package.json
@@ -0,0 +1,15 @@
+{
+  "dependencies": {
+    "axios": "^1.8.4",
+    "dotenv": "^16.5.0",
+    "ethers": "5.7"
+  },
+  "devDependencies": {
+    "@types/node": "^22.14.1",
+    "ts-node": "^10.9.2",
+    "typescript": "^5.8.3"
+  },
+  "scripts": {
+    "start": "ts-node chainlink_monitor_slack_alert.ts"
+  }
+}
diff --git a/utils/chainlink_monitor/pnpm-lock.yaml b/utils/chainlink_monitor/pnpm-lock.yaml
new file mode 100644
index 00000000..37c88924
--- /dev/null
+++ b/utils/chainlink_monitor/pnpm-lock.yaml
@@ -0,0 +1,848 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      axios:
+        specifier: ^1.8.4
+        version: 1.8.4
+      dotenv:
+        specifier: ^16.5.0
+        version: 16.5.0
+      ethers:
+        specifier: '5.7'
+        version: 5.7.2
+    devDependencies:
+      '@types/node':
+        specifier: ^22.14.1
+        version: 22.14.1
+      ts-node:
+        specifier: ^10.9.2
+        version: 10.9.2(@types/node@22.14.1)(typescript@5.8.3)
+      typescript:
+        specifier: ^5.8.3
+        version: 5.8.3
+
+packages:
+
+  '@cspotcode/source-map-support@0.8.1':
+    resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
+    engines: {node: '>=12'}
+
+  '@ethersproject/abi@5.7.0':
+    resolution: {integrity: sha512-351ktp42TiRcYB3H1OP8yajPeAQstMW/yCFokj/AthP9bLHzQFPlOrxOcwYEDkUAICmOHljvN4K39OMTMUa9RA==}
+
+  '@ethersproject/abstract-provider@5.7.0':
+    resolution: {integrity: sha512-R41c9UkchKCpAqStMYUpdunjo3pkEvZC3FAwZn5S5MGbXoMQOHIdHItezTETxAO5bevtMApSyEhn9+CHcDsWBw==}
+
+  '@ethersproject/abstract-signer@5.7.0':
+    resolution: {integrity: sha512-a16V8bq1/Cz+TGCkE2OPMTOUDLS3grCpdjoJCYNnVBbdYEMSgKrU0+B90s8b6H+ByYTBZN7a3g76jdIJi7UfKQ==}
+
+  '@ethersproject/address@5.7.0':
+    resolution: {integrity: sha512-9wYhYt7aghVGo758POM5nqcOMaE168Q6aRLJZwUmiqSrAungkG74gSSeKEIR7ukixesdRZGPgVqme6vmxs1fkA==}
+
+  '@ethersproject/base64@5.7.0':
+    resolution: {integrity: sha512-Dr8tcHt2mEbsZr/mwTPIQAf3Ai0Bks/7gTw9dSqk1mQvhW3XvRlmDJr/4n+wg1JmCl16NZue17CDh8xb/vZ0sQ==}
+
+  '@ethersproject/basex@5.7.0':
+    resolution: {integrity: sha512-ywlh43GwZLv2Voc2gQVTKBoVQ1mti3d8HK5aMxsfu/nRDnMmNqaSJ3r3n85HBByT8OpoY96SXM1FogC533T4zw==}
+
+  '@ethersproject/bignumber@5.7.0':
+    resolution: {integrity: sha512-n1CAdIHRWjSucQO3MC1zPSVgV/6dy/fjL9pMrPP9peL+QxEg9wOsVqwD4+818B6LUEtaXzVHQiuivzRoxPxUGw==}
+
+  '@ethersproject/bytes@5.7.0':
+    resolution: {integrity: sha512-nsbxwgFXWh9NyYWo+U8atvmMsSdKJprTcICAkvbBffT75qDocbuggBU0SJiVK2MuTrp0q+xvLkTnGMPK1+uA9A==}
+
+  '@ethersproject/constants@5.7.0':
+    resolution: {integrity: sha512-DHI+y5dBNvkpYUMiRQyxRBYBefZkJfo70VUkUAsRjcPs47muV9evftfZ0PJVCXYbAiCgght0DtcF9srFQmIgWA==}
+
+  '@ethersproject/contracts@5.7.0':
+    resolution: {integrity: sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg==}
+
+  '@ethersproject/hash@5.7.0':
+    resolution: {integrity: sha512-qX5WrQfnah1EFnO5zJv1v46a8HW0+E5xuBBDTwMFZLuVTx0tbU2kkx15NqdjxecrLGatQN9FGQKpb1FKdHCt+g==}
+
+  '@ethersproject/hdnode@5.7.0':
+    resolution: {integrity: sha512-OmyYo9EENBPPf4ERhR7oj6uAtUAhYGqOnIS+jE5pTXvdKBS99ikzq1E7Iv0ZQZ5V36Lqx1qZLeak0Ra16qpeOg==}
+
+  '@ethersproject/json-wallets@5.7.0':
+    resolution: {integrity: sha512-8oee5Xgu6+RKgJTkvEMl2wDgSPSAQ9MB/3JYjFV9jlKvcYHUXZC+cQp0njgmxdHkYWn8s6/IqIZYm0YWCjO/0g==}
+
+  '@ethersproject/keccak256@5.7.0':
+    resolution: {integrity: sha512-2UcPboeL/iW+pSg6vZ6ydF8tCnv3Iu/8tUmLLzWWGzxWKFFqOBQFLo6uLUv6BDrLgCDfN28RJ/wtByx+jZ4KBg==}
+
+  '@ethersproject/logger@5.7.0':
+    resolution: {integrity: sha512-0odtFdXu/XHtjQXJYA3u9G0G8btm0ND5Cu8M7i5vhEcE8/HmF4Lbdqanwyv4uQTr2tx6b7fQRmgLrsnpQlmnig==}
+
+  '@ethersproject/networks@5.7.1':
+    resolution: {integrity: sha512-n/MufjFYv3yFcUyfhnXotyDlNdFb7onmkSy8aQERi2PjNcnWQ66xXxa3XlS8nCcA8aJKJjIIMNJTC7tu80GwpQ==}
+
+  '@ethersproject/pbkdf2@5.7.0':
+    resolution: {integrity: sha512-oR/dBRZR6GTyaofd86DehG72hY6NpAjhabkhxgr3X2FpJtJuodEl2auADWBZfhDHgVCbu3/H/Ocq2uC6dpNjjw==}
+
+  '@ethersproject/properties@5.7.0':
+    resolution: {integrity: sha512-J87jy8suntrAkIZtecpxEPxY//szqr1mlBaYlQ0r4RCaiD2hjheqF9s1LVE8vVuJCXisjIP+JgtK/Do54ej4Sw==}
+
+  '@ethersproject/providers@5.7.2':
+    resolution: {integrity: sha512-g34EWZ1WWAVgr4aptGlVBF8mhl3VWjv+8hoAnzStu8Ah22VHBsuGzP17eb6xDVRzw895G4W7vvx60lFFur/1Rg==}
+
+  '@ethersproject/random@5.7.0':
+    resolution: {integrity: sha512-19WjScqRA8IIeWclFme75VMXSBvi4e6InrUNuaR4s5pTF2qNhcGdCUwdxUVGtDDqC00sDLCO93jPQoDUH4HVmQ==}
+
+  '@ethersproject/rlp@5.7.0':
+    resolution: {integrity: sha512-rBxzX2vK8mVF7b0Tol44t5Tb8gomOHkj5guL+HhzQ1yBh/ydjGnpw6at+X6Iw0Kp3OzzzkcKp8N9r0W4kYSs9w==}
+
+  '@ethersproject/sha2@5.7.0':
+    resolution: {integrity: sha512-gKlH42riwb3KYp0reLsFTokByAKoJdgFCwI+CCiX/k+Jm2mbNs6oOaCjYQSlI1+XBVejwH2KrmCbMAT/GnRDQw==}
+
+  '@ethersproject/signing-key@5.7.0':
+    resolution: {integrity: sha512-MZdy2nL3wO0u7gkB4nA/pEf8lu1TlFswPNmy8AiYkfKTdO6eXBJyUdmHO/ehm/htHw9K/qF8ujnTyUAD+Ry54Q==}
+
+  '@ethersproject/solidity@5.7.0':
+    resolution: {integrity: sha512-HmabMd2Dt/raavyaGukF4XxizWKhKQ24DoLtdNbBmNKUOPqwjsKQSdV9GQtj9CBEea9DlzETlVER1gYeXXBGaA==}
+
+  '@ethersproject/strings@5.7.0':
+    resolution: {integrity: sha512-/9nu+lj0YswRNSH0NXYqrh8775XNyEdUQAuf3f+SmOrnVewcJ5SBNAjF7lpgehKi4abvNNXyf+HX86czCdJ8Mg==}
+
+  '@ethersproject/transactions@5.7.0':
+    resolution: {integrity: sha512-kmcNicCp1lp8qanMTC3RIikGgoJ80ztTyvtsFvCYpSCfkjhD0jZ2LOrnbcuxuToLIUYYf+4XwD1rP+B/erDIhQ==}
+
+  '@ethersproject/units@5.7.0':
+    resolution: {integrity: sha512-pD3xLMy3SJu9kG5xDGI7+xhTEmGXlEqXU4OfNapmfnxLVY4EMSSRp7j1k7eezutBPH7RBN/7QPnwR7hzNlEFeg==}
+
+  '@ethersproject/wallet@5.7.0':
+    resolution: {integrity: sha512-MhmXlJXEJFBFVKrDLB4ZdDzxcBxQ3rLyCkhNqVu3CDYvR97E+8r01UgrI+TI99Le+aYm/in/0vp86guJuM7FCA==}
+
+  '@ethersproject/web@5.7.1':
+    resolution: {integrity: sha512-Gueu8lSvyjBWL4cYsWsjh6MtMwM0+H4HvqFPZfB6dV8ctbP9zFAO73VG1cMWae0FLPCtz0peKPpZY8/ugJJX2w==}
+
+  '@ethersproject/wordlists@5.7.0':
+    resolution: {integrity: sha512-S2TFNJNfHWVHNE6cNDjbVlZ6MgE17MIxMbMg2zv3wn+3XSJGosL1m9ZVv3GXCf/2ymSsQ+hRI5IzoMJTG6aoVA==}
+
+  '@jridgewell/resolve-uri@3.1.2':
+    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+    engines: {node: '>=6.0.0'}
+
+  '@jridgewell/sourcemap-codec@1.5.0':
+    resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
+
+  '@jridgewell/trace-mapping@0.3.9':
+    resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
+
+  '@tsconfig/node10@1.0.11':
+    resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==}
+
+  '@tsconfig/node12@1.0.11':
+    resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==}
+
+  '@tsconfig/node14@1.0.3':
+    resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==}
+
+  '@tsconfig/node16@1.0.4':
+    resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==}
+
+  '@types/node@22.14.1':
+    resolution: {integrity: sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==}
+
+  acorn-walk@8.3.4:
+    resolution: {integrity: sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==}
+    engines: {node: '>=0.4.0'}
+
+  acorn@8.14.1:
+    resolution: {integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  aes-js@3.0.0:
+    resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==}
+
+  arg@4.1.3:
+    resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==}
+
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+  axios@1.8.4:
+    resolution: {integrity: sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==}
+
+  bech32@1.1.4:
+    resolution: {integrity: sha512-s0IrSOzLlbvX7yp4WBfPITzpAU8sqQcpsmwXDiKwrG4r491vwCO/XpejasRNl0piBMe/DvP4Tz0mIS/X1DPJBQ==}
+
+  bn.js@4.12.1:
+    resolution: {integrity: sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==}
+
+  bn.js@5.2.1:
+    resolution: {integrity: sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==}
+
+  brorand@1.1.0:
+    resolution: {integrity: sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==}
+
+  call-bind-apply-helpers@1.0.2:
+    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+    engines: {node: '>= 0.4'}
+
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
+  create-require@1.1.1:
+    resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==}
+
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
+  diff@4.0.2:
+    resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
+    engines: {node: '>=0.3.1'}
+
+  dotenv@16.5.0:
+    resolution: {integrity: sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==}
+    engines: {node: '>=12'}
+
+  dunder-proto@1.0.1:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
+  elliptic@6.5.4:
+    resolution: {integrity: sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==}
+
+  es-define-property@1.0.1:
+    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+    engines: {node: '>= 0.4'}
+
+  es-errors@1.3.0:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
+  es-object-atoms@1.1.1:
+    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+    engines: {node: '>= 0.4'}
+
+  es-set-tostringtag@2.1.0:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
+  ethers@5.7.2:
+    resolution: {integrity: sha512-wswUsmWo1aOK8rR7DIKiWSw9DbLWe6x98Jrn8wcTflTVvaXhAMaB5zGAXy0GYQEQp9iO1iSHWVyARQm11zUtyg==}
+
+  follow-redirects@1.15.9:
+    resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
+  form-data@4.0.2:
+    resolution: {integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==}
+    engines: {node: '>= 6'}
+
+  function-bind@1.1.2:
+    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+  get-intrinsic@1.3.0:
+    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+    engines: {node: '>= 0.4'}
+
+  get-proto@1.0.1:
+    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+    engines: {node: '>= 0.4'}
+
+  gopd@1.2.0:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
+
+  has-symbols@1.1.0:
+    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+    engines: {node: '>= 0.4'}
+
+  has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
+  hash.js@1.1.7:
+    resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
+
+  hasown@2.0.2:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    engines: {node: '>= 0.4'}
+
+  hmac-drbg@1.0.1:
+    resolution: {integrity: sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==}
+
+  inherits@2.0.4:
+    resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
+
+  js-sha3@0.8.0:
+    resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==}
+
+  make-error@1.3.6:
+    resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==}
+
+  math-intrinsics@1.1.0:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
+  mime-db@1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+
+  mime-types@2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+
+  minimalistic-assert@1.0.1:
+    resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
+
+  minimalistic-crypto-utils@1.0.1:
+    resolution: {integrity: sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==}
+
+  proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+  scrypt-js@3.0.1:
+    resolution: {integrity: sha512-cdwTTnqPu0Hyvf5in5asVdZocVDTNRmR7XEcJuIzMjJeSHybHl7vpB66AzwTaIg6CLSbtjcxc8fqcySfnTkccA==}
+
+  ts-node@10.9.2:
+    resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==}
+    hasBin: true
+    peerDependencies:
+      '@swc/core': '>=1.2.50'
+      '@swc/wasm': '>=1.2.50'
+      '@types/node': '*'
+      typescript: '>=2.7'
+    peerDependenciesMeta:
+      '@swc/core':
+        optional: true
+      '@swc/wasm':
+        optional: true
+
+  typescript@5.8.3:
+    resolution: {integrity: sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  undici-types@6.21.0:
+    resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+  v8-compile-cache-lib@3.0.1:
+    resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==}
+
+  ws@7.4.6:
+    resolution: {integrity: sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==}
+    engines: {node: '>=8.3.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: ^5.0.2
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+
+  yn@3.1.1:
+    resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
+    engines: {node: '>=6'}
+
+snapshots:
+
+  '@cspotcode/source-map-support@0.8.1':
+    dependencies:
+      '@jridgewell/trace-mapping': 0.3.9
+
+  '@ethersproject/abi@5.7.0':
+    dependencies:
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/hash': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/strings': 5.7.0
+
+  '@ethersproject/abstract-provider@5.7.0':
+    dependencies:
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/networks': 5.7.1
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+      '@ethersproject/web': 5.7.1
+
+  '@ethersproject/abstract-signer@5.7.0':
+    dependencies:
+      '@ethersproject/abstract-provider': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+
+  '@ethersproject/address@5.7.0':
+    dependencies:
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/rlp': 5.7.0
+
+  '@ethersproject/base64@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+
+  '@ethersproject/basex@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/properties': 5.7.0
+
+  '@ethersproject/bignumber@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      bn.js: 5.2.1
+
+  '@ethersproject/bytes@5.7.0':
+    dependencies:
+      '@ethersproject/logger': 5.7.0
+
+  '@ethersproject/constants@5.7.0':
+    dependencies:
+      '@ethersproject/bignumber': 5.7.0
+
+  '@ethersproject/contracts@5.7.0':
+    dependencies:
+      '@ethersproject/abi': 5.7.0
+      '@ethersproject/abstract-provider': 5.7.0
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+
+  '@ethersproject/hash@5.7.0':
+    dependencies:
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/base64': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/strings': 5.7.0
+
+  '@ethersproject/hdnode@5.7.0':
+    dependencies:
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/basex': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/pbkdf2': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/sha2': 5.7.0
+      '@ethersproject/signing-key': 5.7.0
+      '@ethersproject/strings': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+      '@ethersproject/wordlists': 5.7.0
+
+  '@ethersproject/json-wallets@5.7.0':
+    dependencies:
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/hdnode': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/pbkdf2': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/random': 5.7.0
+      '@ethersproject/strings': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+      aes-js: 3.0.0
+      scrypt-js: 3.0.1
+
+  '@ethersproject/keccak256@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      js-sha3: 0.8.0
+
+  '@ethersproject/logger@5.7.0': {}
+
+  '@ethersproject/networks@5.7.1':
+    dependencies:
+      '@ethersproject/logger': 5.7.0
+
+  '@ethersproject/pbkdf2@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/sha2': 5.7.0
+
+  '@ethersproject/properties@5.7.0':
+    dependencies:
+      '@ethersproject/logger': 5.7.0
+
+  '@ethersproject/providers@5.7.2':
+    dependencies:
+      '@ethersproject/abstract-provider': 5.7.0
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/base64': 5.7.0
+      '@ethersproject/basex': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/hash': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/networks': 5.7.1
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/random': 5.7.0
+      '@ethersproject/rlp': 5.7.0
+      '@ethersproject/sha2': 5.7.0
+      '@ethersproject/strings': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+      '@ethersproject/web': 5.7.1
+      bech32: 1.1.4
+      ws: 7.4.6
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  '@ethersproject/random@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+
+  '@ethersproject/rlp@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+
+  '@ethersproject/sha2@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      hash.js: 1.1.7
+
+  '@ethersproject/signing-key@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      bn.js: 5.2.1
+      elliptic: 6.5.4
+      hash.js: 1.1.7
+
+  '@ethersproject/solidity@5.7.0':
+    dependencies:
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/sha2': 5.7.0
+      '@ethersproject/strings': 5.7.0
+
+  '@ethersproject/strings@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/logger': 5.7.0
+
+  '@ethersproject/transactions@5.7.0':
+    dependencies:
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/rlp': 5.7.0
+      '@ethersproject/signing-key': 5.7.0
+
+  '@ethersproject/units@5.7.0':
+    dependencies:
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/logger': 5.7.0
+
+  '@ethersproject/wallet@5.7.0':
+    dependencies:
+      '@ethersproject/abstract-provider': 5.7.0
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/hash': 5.7.0
+      '@ethersproject/hdnode': 5.7.0
+      '@ethersproject/json-wallets': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/random': 5.7.0
+      '@ethersproject/signing-key': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+      '@ethersproject/wordlists': 5.7.0
+
+  '@ethersproject/web@5.7.1':
+    dependencies:
+      '@ethersproject/base64': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/strings': 5.7.0
+
+  '@ethersproject/wordlists@5.7.0':
+    dependencies:
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/hash': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/strings': 5.7.0
+
+  '@jridgewell/resolve-uri@3.1.2': {}
+
+  '@jridgewell/sourcemap-codec@1.5.0': {}
+
+  '@jridgewell/trace-mapping@0.3.9':
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.2
+      '@jridgewell/sourcemap-codec': 1.5.0
+
+  '@tsconfig/node10@1.0.11': {}
+
+  '@tsconfig/node12@1.0.11': {}
+
+  '@tsconfig/node14@1.0.3': {}
+
+  '@tsconfig/node16@1.0.4': {}
+
+  '@types/node@22.14.1':
+    dependencies:
+      undici-types: 6.21.0
+
+  acorn-walk@8.3.4:
+    dependencies:
+      acorn: 8.14.1
+
+  acorn@8.14.1: {}
+
+  aes-js@3.0.0: {}
+
+  arg@4.1.3: {}
+
+  asynckit@0.4.0: {}
+
+  axios@1.8.4:
+    dependencies:
+      follow-redirects: 1.15.9
+      form-data: 4.0.2
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
+  bech32@1.1.4: {}
+
+  bn.js@4.12.1: {}
+
+  bn.js@5.2.1: {}
+
+  brorand@1.1.0: {}
+
+  call-bind-apply-helpers@1.0.2:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
+  create-require@1.1.1: {}
+
+  delayed-stream@1.0.0: {}
+
+  diff@4.0.2: {}
+
+  dotenv@16.5.0: {}
+
+  dunder-proto@1.0.1:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
+  elliptic@6.5.4:
+    dependencies:
+      bn.js: 4.12.1
+      brorand: 1.1.0
+      hash.js: 1.1.7
+      hmac-drbg: 1.0.1
+      inherits: 2.0.4
+      minimalistic-assert: 1.0.1
+      minimalistic-crypto-utils: 1.0.1
+
+  es-define-property@1.0.1: {}
+
+  es-errors@1.3.0: {}
+
+  es-object-atoms@1.1.1:
+    dependencies:
+      es-errors: 1.3.0
+
+  es-set-tostringtag@2.1.0:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
+  ethers@5.7.2:
+    dependencies:
+      '@ethersproject/abi': 5.7.0
+      '@ethersproject/abstract-provider': 5.7.0
+      '@ethersproject/abstract-signer': 5.7.0
+      '@ethersproject/address': 5.7.0
+      '@ethersproject/base64': 5.7.0
+      '@ethersproject/basex': 5.7.0
+      '@ethersproject/bignumber': 5.7.0
+      '@ethersproject/bytes': 5.7.0
+      '@ethersproject/constants': 5.7.0
+      '@ethersproject/contracts': 5.7.0
+      '@ethersproject/hash': 5.7.0
+      '@ethersproject/hdnode': 5.7.0
+      '@ethersproject/json-wallets': 5.7.0
+      '@ethersproject/keccak256': 5.7.0
+      '@ethersproject/logger': 5.7.0
+      '@ethersproject/networks': 5.7.1
+      '@ethersproject/pbkdf2': 5.7.0
+      '@ethersproject/properties': 5.7.0
+      '@ethersproject/providers': 5.7.2
+      '@ethersproject/random': 5.7.0
+      '@ethersproject/rlp': 5.7.0
+      '@ethersproject/sha2': 5.7.0
+      '@ethersproject/signing-key': 5.7.0
+      '@ethersproject/solidity': 5.7.0
+      '@ethersproject/strings': 5.7.0
+      '@ethersproject/transactions': 5.7.0
+      '@ethersproject/units': 5.7.0
+      '@ethersproject/wallet': 5.7.0
+      '@ethersproject/web': 5.7.1
+      '@ethersproject/wordlists': 5.7.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
+  follow-redirects@1.15.9: {}
+
+  form-data@4.0.2:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      mime-types: 2.1.35
+
+  function-bind@1.1.2: {}
+
+  get-intrinsic@1.3.0:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-define-property: 1.0.1
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      function-bind: 1.1.2
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-symbols: 1.1.0
+      hasown: 2.0.2
+      math-intrinsics: 1.1.0
+
+  get-proto@1.0.1:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.1
+
+  gopd@1.2.0: {}
+
+  has-symbols@1.1.0: {}
+
+  has-tostringtag@1.0.2:
+    dependencies:
+      has-symbols: 1.1.0
+
+  hash.js@1.1.7:
+    dependencies:
+      inherits: 2.0.4
+      minimalistic-assert: 1.0.1
+
+  hasown@2.0.2:
+    dependencies:
+      function-bind: 1.1.2
+
+  hmac-drbg@1.0.1:
+    dependencies:
+      hash.js: 1.1.7
+      minimalistic-assert: 1.0.1
+      minimalistic-crypto-utils: 1.0.1
+
+  inherits@2.0.4: {}
+
+  js-sha3@0.8.0: {}
+
+  make-error@1.3.6: {}
+
+  math-intrinsics@1.1.0: {}
+
+  mime-db@1.52.0: {}
+
+  mime-types@2.1.35:
+    dependencies:
+      mime-db: 1.52.0
+
+  minimalistic-assert@1.0.1: {}
+
+  minimalistic-crypto-utils@1.0.1: {}
+
+  proxy-from-env@1.1.0: {}
+
+  scrypt-js@3.0.1: {}
+
+  ts-node@10.9.2(@types/node@22.14.1)(typescript@5.8.3):
+    dependencies:
+      '@cspotcode/source-map-support': 0.8.1
+      '@tsconfig/node10': 1.0.11
+      '@tsconfig/node12': 1.0.11
+      '@tsconfig/node14': 1.0.3
+      '@tsconfig/node16': 1.0.4
+      '@types/node': 22.14.1
+      acorn: 8.14.1
+      acorn-walk: 8.3.4
+      arg: 4.1.3
+      create-require: 1.1.1
+      diff: 4.0.2
+      make-error: 1.3.6
+      typescript: 5.8.3
+      v8-compile-cache-lib: 3.0.1
+      yn: 3.1.1
+
+  typescript@5.8.3: {}
+
+  undici-types@6.21.0: {}
+
+  v8-compile-cache-lib@3.0.1: {}
+
+  ws@7.4.6: {}
+
+  yn@3.1.1: {}
diff --git a/utils/chainlink_monitor/send_slack.ts b/utils/chainlink_monitor/send_slack.ts
new file mode 100644
index 00000000..eeee926a
--- /dev/null
+++ b/utils/chainlink_monitor/send_slack.ts
@@ -0,0 +1,79 @@
+import axios from "axios";
+import { SLACK_WEBHOOK_URL } from "./chainlink_monitor_slack_alert";
+
+type AlertEventData = {
+  message: string;
+  timestamp: number;
+  blockNumber: number;
+  txHash: string;
+  shouldSendTx?: boolean;
+  header?: string;
+};
+
+export async function sendSlackAlert(eventData: AlertEventData): Promise<void> {
+  try {
+    const message = {
+      blocks: [
+        {
+          type: "header",
+          text: {
+            type: "plain_text",
+            text:
+              eventData.header ??
+              "🚨 Mismatch found between Chainlink Automation and On-Chain sorting",
+            emoji: true,
+          },
+        },
+        {
+          type: "section",
+          text: {
+            type: "mrkdwn",
+            text: `${eventData.message}`,
+          },
+        },
+        ...(eventData.shouldSendTx
+          ? [
+              {
+                type: "section",
+                text: {
+                  type: "mrkdwn",
+                  text: `*Etherscan Link:*\n<https://etherscan.io/tx/${eventData.txHash}|View Transaction on Etherscan>`,
+                },
+              },
+            ]
+          : []),
+        {
+          type: "section",
+          text: {
+            type: "mrkdwn",
+            text: `*Block Number:*\n${eventData.blockNumber}`,
+          },
+        },
+        {
+          type: "section",
+          text: {
+            type: "mrkdwn",
+            text: `*Timestamp:*\n${new Date(
+              eventData.timestamp * 1000
+            ).toISOString()}`,
+          },
+        },
+        {
+          type: "section",
+          text: {
+            type: "mrkdwn",
+            text: `*Transaction:*\n\`${eventData.txHash}\``,
+          },
+        },
+        {
+          type: "divider",
+        },
+      ],
+    };
+
+    await axios.post(SLACK_WEBHOOK_URL as string, message);
+    console.log("Alert sent to Slack successfully");
+  } catch (error) {
+    console.error("Failed to send Slack alert:", error);
+  }
+}
diff --git a/utils/chainlink_monitor/tsconfig.json b/utils/chainlink_monitor/tsconfig.json
new file mode 100644
index 00000000..0709968e
--- /dev/null
+++ b/utils/chainlink_monitor/tsconfig.json
@@ -0,0 +1,15 @@
+{
+  "compilerOptions": {
+    "target": "ES2020",
+    "module": "commonjs",
+    "strict": true,
+    "esModuleInterop": true,
+    "skipLibCheck": true,
+    "forceConsistentCasingInFileNames": true,
+    "outDir": "dist",
+    "declaration": true,
+    "resolveJsonModule": true
+  },
+  "include": ["*.ts"],
+  "exclude": ["node_modules", "dist"]
+}
diff --git a/utils/checkStorage.js b/utils/checkStorage/checkStorage.js
similarity index 100%
rename from utils/checkStorage.js
rename to utils/checkStorage/checkStorage.js
diff --git a/utils/package.json b/utils/checkStorage/package.json
similarity index 100%
rename from utils/package.json
rename to utils/checkStorage/package.json
diff --git a/utils/pnpm-lock.yaml b/utils/checkStorage/pnpm-lock.yaml
similarity index 100%
rename from utils/pnpm-lock.yaml
rename to utils/checkStorage/pnpm-lock.yaml


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: 

Copilot reviewed 10 out of 12 changed files in this pull request and generated 1 comment.

<details>
<summary>Files not reviewed (2)</summary>

* **utils/chainlink_monitor/package.json**: Language not supported
* **utils/chainlink_monitor/tsconfig.json**: Language not supported
</details>



state: COMMENTED
author_association: NONE
submitted_at: 2025-04-16T03:49:21Z
user: login: NindoK
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T03:52:31Z
user: login: steven2308
body: Pretty clean job, love it.
Once I understand why these mock data is added I'll approve.
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T12:35:25Z
user: login: steven2308
body: Awesome!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T13:47:29Z
user: login: steven2308
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:08:46Z
user: login: steven2308
body: This is my last approve today :laughing: 
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-16T16:11:21Z
user: login: evgeny-s
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T08:23:13Z
user: login: evgeny-s
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T08:24:10Z
user: login: evgeny-s
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T10:05:06Z

Review comments:
diff_hunk: @@ -0,0 +1,211 @@
+import { ethers } from "ethers";
+import * as dotenv from "dotenv";
+import { compareWithMiddlewareData } from "./compare";
+
+dotenv.config();
+
+// Environment variables
+const RPC_URL = process.env.RPC_URL;
+const GATEWAY_ADDRESS = process.env.GATEWAY_ADDRESS;
+const MIDDLEWARE_ADDRESS = process.env.MIDDLEWARE_ADDRESS;
+export const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
+
+// Gateway ABI - just the parts we need for the event
+const gatewayAbi = [
+  "event OperatorsDataCreated(uint256 indexed validatorsCount, bytes payload)",
+];
+
+// Middleware ABI - just the parts we need for functions
+const middlewareAbi = [
+  {
+    inputs: [{ internalType: "uint48", name: "epoch", type: "uint48" }],
+    stateMutability: "view",
+    type: "function",
+    name: "sortOperatorsByPower",
+    outputs: [
+      {
+        internalType: "bytes32[]",
+        name: "sortedKeys",
+        type: "bytes32[]",
+      },
+    ],
+  },
+  {
+    type: "function",
+    name: "getEpochAtTs",
+    inputs: [{ name: "timestamp", type: "uint48", internalType: "uint48" }],
+    outputs: [{ name: "epoch", type: "uint48", internalType: "uint48" }],
+    stateMutability: "view",
+  },
+  // TODO take out. Should use sortOperatorsByPower once Middleware is upgraded to latest
+  {
+    inputs: [{ internalType: "uint48", name: "epoch", type: "uint48" }],
+    name: "sortOperatorsByVaults",
+    outputs: [
+      { internalType: "bytes32[]", name: "sortedKeys", type: "bytes32[]" },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+];
+
+async function main(): Promise<void> {
+  if (
+    !RPC_URL ||
+    !GATEWAY_ADDRESS ||
+    !SLACK_WEBHOOK_URL ||
+    !MIDDLEWARE_ADDRESS
+  ) {
+    throw new Error(
+      "Missing required environment variables. Check your .env file."
+    );
+  }
+
+  // Parse command line arguments for lookback time in seconds
+  const lookbackSeconds = parseLookbackArgument();
+
+  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
+
+  // Gateway contract instance
+  const gatewayContract = new ethers.Contract(
+    GATEWAY_ADDRESS,
+    gatewayAbi,
+    provider
+  );
+
+  // Middleware contract instance
+  const middlewareContract = new ethers.Contract(
+    MIDDLEWARE_ADDRESS,
+    middlewareAbi,
+    provider
+  );
+
+  // If lookback is specified, first query historical events
+  if (lookbackSeconds > 0) {
+    await queryHistoricalEvents(
+      provider,
+      gatewayContract,
+      middlewareContract,
+      lookbackSeconds
+    );
+  }
+
+  console.log(`Monitoring events from Gateway at ${GATEWAY_ADDRESS}...`);
+
+  // Listen for OperatorsDataCreated events
+  gatewayContract.on(
+    "OperatorsDataCreated",
+    async (
+      validatorsCount: ethers.BigNumber,
+      payload: string,
+      event: ethers.Event
+    ) => {
+      const blockNumber = event.blockNumber;
+      const txHash = event.transactionHash;
+
+      console.log(`
+      Event detected at block ${blockNumber}
+      Transaction: ${txHash}
+      Validators Count: ${validatorsCount.toString()}
+      Payload: ${payload}
+    `);
+      const currentBlockData = await provider.getBlock(event.blockNumber);
+      const timestamp = currentBlockData.timestamp;
+
+      await compareWithMiddlewareData(
+        middlewareContract,
+        payload,
+        timestamp,
+        event
+      );
+    }
+  );
+}
+
+// Parse command line argument for lookback time
+function parseLookbackArgument(): number {
+  const args = process.argv.slice(2);
+  if (args.length === 0) {
+    return 0; // Default: no lookback
+  }
+
+  const lookbackSeconds = parseInt(args[1], 10);
path: utils/chainlink_monitor/chainlink_monitor_slack_alert.ts
user: login: Copilot
body: The command line argument indexing is incorrect; 'args[1]' should likely be 'args[0]' to correctly capture the lookback period as only one argument is passed after slicing process.argv.
```suggestion
  const lookbackSeconds = parseInt(args[0], 10);
```
created_at: 2025-04-16T03:49:21Z
author_association: NONE
diff_hunk: @@ -0,0 +1,211 @@
+import { ethers } from "ethers";
+import * as dotenv from "dotenv";
+import { compareWithMiddlewareData } from "./compare";
+
+dotenv.config();
+
+// Environment variables
+const RPC_URL = process.env.RPC_URL;
+const GATEWAY_ADDRESS = process.env.GATEWAY_ADDRESS;
+const MIDDLEWARE_ADDRESS = process.env.MIDDLEWARE_ADDRESS;
+export const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;
+
+// Gateway ABI - just the parts we need for the event
+const gatewayAbi = [
+  "event OperatorsDataCreated(uint256 indexed validatorsCount, bytes payload)",
+];
+
+// Middleware ABI - just the parts we need for functions
+const middlewareAbi = [
+  {
+    inputs: [{ internalType: "uint48", name: "epoch", type: "uint48" }],
+    stateMutability: "view",
+    type: "function",
+    name: "sortOperatorsByPower",
+    outputs: [
+      {
+        internalType: "bytes32[]",
+        name: "sortedKeys",
+        type: "bytes32[]",
+      },
+    ],
+  },
+  {
+    type: "function",
+    name: "getEpochAtTs",
+    inputs: [{ name: "timestamp", type: "uint48", internalType: "uint48" }],
+    outputs: [{ name: "epoch", type: "uint48", internalType: "uint48" }],
+    stateMutability: "view",
+  },
+  // TODO take out. Should use sortOperatorsByPower once Middleware is upgraded to latest
+  {
+    inputs: [{ internalType: "uint48", name: "epoch", type: "uint48" }],
+    name: "sortOperatorsByVaults",
+    outputs: [
+      { internalType: "bytes32[]", name: "sortedKeys", type: "bytes32[]" },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+];
+
+async function main(): Promise<void> {
+  if (
+    !RPC_URL ||
+    !GATEWAY_ADDRESS ||
+    !SLACK_WEBHOOK_URL ||
+    !MIDDLEWARE_ADDRESS
+  ) {
+    throw new Error(
+      "Missing required environment variables. Check your .env file."
+    );
+  }
+
+  // Parse command line arguments for lookback time in seconds
+  const lookbackSeconds = parseLookbackArgument();
+
+  const provider = new ethers.providers.JsonRpcProvider(RPC_URL);
+
+  // Gateway contract instance
+  const gatewayContract = new ethers.Contract(
+    GATEWAY_ADDRESS,
+    gatewayAbi,
+    provider
+  );
+
+  // Middleware contract instance
+  const middlewareContract = new ethers.Contract(
+    MIDDLEWARE_ADDRESS,
+    middlewareAbi,
+    provider
+  );
+
+  // If lookback is specified, first query historical events
+  if (lookbackSeconds > 0) {
+    await queryHistoricalEvents(
+      provider,
+      gatewayContract,
+      middlewareContract,
+      lookbackSeconds
+    );
+  }
+
+  console.log(`Monitoring events from Gateway at ${GATEWAY_ADDRESS}...`);
+
+  // Listen for OperatorsDataCreated events
+  gatewayContract.on(
+    "OperatorsDataCreated",
+    async (
+      validatorsCount: ethers.BigNumber,
+      payload: string,
+      event: ethers.Event
+    ) => {
+      const blockNumber = event.blockNumber;
+      const txHash = event.transactionHash;
+
+      console.log(`
+      Event detected at block ${blockNumber}
+      Transaction: ${txHash}
+      Validators Count: ${validatorsCount.toString()}
+      Payload: ${payload}
+    `);
+      const currentBlockData = await provider.getBlock(event.blockNumber);
+      const timestamp = currentBlockData.timestamp;
+
+      await compareWithMiddlewareData(
+        middlewareContract,
+        payload,
+        timestamp,
+        event
+      );
+    }
+  );
+}
+
+// Parse command line argument for lookback time
+function parseLookbackArgument(): number {
+  const args = process.argv.slice(2);
+  if (args.length === 0) {
+    return 0; // Default: no lookback
+  }
+
+  const lookbackSeconds = parseInt(args[1], 10);
path: utils/chainlink_monitor/chainlink_monitor_slack_alert.ts
user: login: NindoK
body: This is wrong since when calling the script we are passing as args the lookback in seconds. To do that (since we are using a `script` defined in `package.json` we need to use `-- 3600`. -- will be at args[0].
created_at: 2025-04-16T03:52:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,135 @@
+import { ethers } from "ethers";
+import { sendSlackAlert } from "./send_slack";
+import { decodeOperatorsData } from "./decode_operators_data";
+
+export async function compareWithMiddlewareData(
+  middlewareContract: ethers.Contract,
+  payload: string,
+  timestamp: number,
+  event: ethers.Event
+): Promise<void> {
+  const epoch = await middlewareContract.getEpochAtTs(timestamp);
+  console.log("Epoch:", epoch.toString());
+  try {
+    // Get sorted operators from middleware
+    // TODO Should use sortOperatorsByPower once Middleware is upgraded to latest
+    const sortedOperators = await middlewareContract.sortOperatorsByVaults(
+      epoch
+    );
+
+    const {
+      operatorsKeys,
+      operatorsCount,
+      epoch: epochValue,
+    } = decodeOperatorsData(payload);
+
+    const operatorsArray = [...sortedOperators];
+
+    operatorsArray.push("0xNewOperatorAddressHere");
+    operatorsArray.push("0xNewOperatorAddressHere");
+    operatorsArray.push("0xNewOperatorAddressHere2");
+
+    operatorsKeys.push("0x000000000000000000000000000000000000dEaD");
+    operatorsKeys.push("0xNewOperatorAddressHere");
+    operatorsKeys.push("0xNewOperatorAddressHere3");
path: utils/chainlink_monitor/compare.ts
user: login: steven2308
body: Why are you adding mock data on both arrays?
created_at: 2025-04-16T12:29:26Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,83 @@
+import { ethers } from "ethers";
+
+export function decodeOperatorsData(payload: string): {
+  operatorsKeys: string[];
+  operatorsCount: number;
+  epoch: number;
+} {
path: utils/chainlink_monitor/decode_operators_data.ts
user: login: steven2308
body: Can we add return type here?
created_at: 2025-04-16T12:33:17Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,12 +1,32 @@
 name: Deploy Tanssi Ecosystem Demo
 
 on:
+  push:
+    branches:
+      - staging
+      - main
   pull_request:
   workflow_dispatch:
 
 jobs:
+  detect-changes:
path: .github/workflows/demo-test.yml
user: login: evgeny-s
body: I don't think we need an extra run here; use the [path filter](https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#onpushpull_requestpull_request_targetpathspaths-ignore):
```
  pull_request:
    paths:
      - '**/*.sol'
```
created_at: 2025-04-29T08:23:13Z
author_association: CONTRIBUTOR
diff_hunk: @@ -37,6 +39,19 @@ jobs:
             echo "coverage_dir=tanssi-symbiotic-coverage/branches/master" >> $GITHUB_OUTPUT
             echo "coverage_report=false" >> $GITHUB_OUTPUT
           fi
+  detect-changes:
path: .github/workflows/test.yml
user: login: evgeny-s
body: Same as above
created_at: 2025-04-29T08:24:09Z
author_association: CONTRIBUTOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-04-16T16:30:58Z
body: <h2>> Coverage generated</h2>Coverage after merging <b>script/monitor-gateway-chainlink-automation</b> into <b>staging</b> will be<table><tbody><tr><th>100.00%</th><th> 0.00%</th></tr></tbody></table>

<details><summary>Coverage Report</summary><table><tbody><tr><th>File</th><th>Stmts</th><th>Branches</th><th>Funcs</th><th>Lines</th><th>Uncovered Lines</th></tr><tr><td colspan='6'><b>script</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/script/DeployCollateral.s.sol'>DeployCollateral.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/script/DeployRewards.s.sol'>DeployRewards.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/script/DeploySymbiotic.s.sol'>DeploySymbiotic.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/script/DeployTanssiEcosystem.s.sol'>DeployTanssiEcosystem.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/script/DeployVault.s.sol'>DeployVault.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/script/HelperConfig.s.sol'>HelperConfig.s.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>src/contracts/extensions</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/src/contracts/extensions/OSharedVaults.sol'>OSharedVaults.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>src/contracts/libraries</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/src/contracts/libraries/QuickSort.sol'>QuickSort.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>src/contracts/middleware</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/src/contracts/middleware/Middleware.sol'>Middleware.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/src/contracts/middleware/MiddlewareStorage.sol'>MiddlewareStorage.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/src/contracts/middleware/OBaseMiddlewareReader.sol'>OBaseMiddlewareReader.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>src/contracts/rewarder</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/src/contracts/rewarder/ODefaultOperatorRewards.sol'>ODefaultOperatorRewards.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/src/contracts/rewarder/ODefaultStakerRewards.sol'>ODefaultStakerRewards.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/src/contracts/rewarder/ODefaultStakerRewardsFactory.sol'>ODefaultStakerRewardsFactory.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>test/mocks</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/test/mocks/FeeToken.sol'>FeeToken.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/test/mocks/Token.sol'>Token.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>test/mocks/symbiotic</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/test/mocks/symbiotic/DelegatorMock.sol'>DelegatorMock.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/test/mocks/symbiotic/RegistryMock.sol'>RegistryMock.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/test/mocks/symbiotic/SharedVaultMock.sol'>SharedVaultMock.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td colspan='6'><b>test/unit/utils</b></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/test/unit/utils/MiddlewareV2.sol'>MiddlewareV2.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr><tr><td>&nbsp; &nbsp;<a href='https://github.com/moondance-labs/tanssi-symbiotic/blob/4ff4c823a530898992ab1cfae8990290b667537a/test/unit/utils/MiddlewareV3.sol'>MiddlewareV3.sol</a></td><td>100%</td><td>100%</td><td>100%</td><td>100%</td><td></td></tr></tbody></table></details>
author_association: NONE
user: login: evgeny-s
created_at: 2025-04-29T10:04:56Z
body: **Note regarding CI fix for detecting changes for `*.sol` files:**
CI still expects this job to finish successfully.
So if we change not *.sol files, the job won’t trigger.
So we have 2 options:
- Keep the implementation with the paths filter, but we must trigger the jobs by changing `*.sol` files.
- Keep the previous implementation. It is a bit more flexible, but has more code.
So, if we are ok with option 1, we can change it later, because the less code the better.
author_association: CONTRIBUTOR