Pārlūkot izejas kodu

chore(price-puhser) Use Hermes Client (#2312)

* initial commit

* update-2

* update-3

* removed test

* wip

* update

* update

* update

* ton-config-update

* 1.0.0

* version bump

* update
Aditya Arora 9 mēneši atpakaļ
vecāks
revīzija
7db2e3a566
32 mainītis faili ar 693 papildinājumiem un 462 dzēšanām
  1. 1 2
      apps/price_pusher/README.md
  2. 1 1
      apps/price_pusher/config.aptos.testnet.sample.json
  3. 0 0
      apps/price_pusher/config.evm.mainnet.sample.json
  4. 1 1
      apps/price_pusher/config.injective.testnet.sample.json
  5. 3 3
      apps/price_pusher/config.sui.mainnet.sample.json
  6. 1 1
      apps/price_pusher/config.ton.mainnet.sample.json
  7. 2 3
      apps/price_pusher/package.json
  8. 5 0
      apps/price_pusher/price-config.stable.sample.yaml
  9. 0 101
      apps/price_pusher/src/__tests__/pyth-price-listener.test.ts
  10. 8 7
      apps/price_pusher/src/aptos/aptos.ts
  11. 21 17
      apps/price_pusher/src/aptos/command.ts
  12. 1 1
      apps/price_pusher/src/controller.ts
  13. 24 17
      apps/price_pusher/src/evm/command.ts
  14. 17 12
      apps/price_pusher/src/evm/evm.ts
  15. 20 16
      apps/price_pusher/src/fuel/command.ts
  16. 7 8
      apps/price_pusher/src/fuel/fuel.ts
  17. 21 17
      apps/price_pusher/src/injective/command.ts
  18. 6 6
      apps/price_pusher/src/injective/injective.ts
  19. 1 1
      apps/price_pusher/src/interface.ts
  20. 21 16
      apps/price_pusher/src/near/command.ts
  21. 6 7
      apps/price_pusher/src/near/near.ts
  22. 1 11
      apps/price_pusher/src/options.ts
  23. 1 1
      apps/price_pusher/src/price-config.ts
  24. 42 126
      apps/price_pusher/src/pyth-price-listener.ts
  25. 22 18
      apps/price_pusher/src/solana/command.ts
  26. 13 8
      apps/price_pusher/src/solana/solana.ts
  27. 22 19
      apps/price_pusher/src/sui/command.ts
  28. 12 10
      apps/price_pusher/src/sui/sui.ts
  29. 20 15
      apps/price_pusher/src/ton/command.ts
  30. 6 5
      apps/price_pusher/src/ton/ton.ts
  31. 31 1
      apps/price_pusher/src/utils.ts
  32. 356 11
      pnpm-lock.yaml

+ 1 - 2
apps/price_pusher/README.md

@@ -203,8 +203,7 @@ human-readable logs, you can pipe the output of the program to `pino-pretty`. Se
 
 You can configure the log level of some of the modules of the price pusher as well. The available modules are PriceServiceConnection, which
 is responsible for connecting to the Hermes price service, and Controller, which is responsible for checking the prices from the Hermes
-and the on-chain Pyth contract and deciding whether to push a new price. You can configure the log level of these modules by passing the
-`--price-service-connection-log-level` and `--controller-log-level` arguments, respectively.
+and the on-chain Pyth contract and deciding whether to push a new price. You can configure the log level of these modules by passing the `--controller-log-level` arguments, respectively.
 
 ### Example
 

+ 1 - 1
apps/price_pusher/config.aptos.testnet.sample.json

@@ -1,5 +1,5 @@
 {
-  "endpoint": "https://fullnode.testnet.aptoslabs.com/v1",
+  "endpoint": "https://api.testnet.aptoslabs.com/v1",
   "pyth-contract-address": "0x7e783b349d3e89cf5931af376ebeadbfab855b3fa239b7ada8f5a92fbea6b387",
   "price-service-endpoint": "https://hermes-beta.pyth.network",
   "mnemonic-file": "./mnemonic",

+ 0 - 0
apps/price_pusher/config.evm.stable.sample.json → apps/price_pusher/config.evm.mainnet.sample.json


+ 1 - 1
apps/price_pusher/config.injective.testnet.sample.json

@@ -1,6 +1,6 @@
 {
   "grpc-endpoint": "https://k8s.testnet.chain.grpc-web.injective.network",
-  "pyth-contract-address": "inj1z60tg0tekdzcasenhuuwq3htjcd5slmgf7gpez",
+  "pyth-contract-address": "inj18rlflp3735h25jmjx97d22c72sxk260amdjxlu",
   "price-service-endpoint": "https://hermes-beta.pyth.network",
   "mnemonic-file": "./mnemonic",
   "price-config-file": "./price-config.beta.sample.yaml",

+ 3 - 3
apps/price_pusher/config.sui.mainnet.sample.json

@@ -1,7 +1,7 @@
 {
-  "endpoint": "https://sui-testnet-rpc.allthatnode.com",
-  "pyth-package-id": "0x00b53b0f4174108627fbee72e2498b58d6a2714cded53fac537034c220d26302",
-  "pyth-state-id": "0xf9ff3ef935ef6cdfb659a203bf2754cebeb63346e29114a535ea6f41315e5a3f",
+  "endpoint": "https://sui-mainnet-endpoint.blockvision.org",
+  "pyth-package-id": "0x04e20ddf36af412a4096f9014f4a565af9e812db9a05cc40254846cf6ed0ad91",
+  "pyth-state-id": "0x1f9310238ee9298fb703c3419030b35b22bb1cc37113e3bb5007c99aec79e5b8",
   "wormhole-package-id": "0x5306f64e312b581766351c07af79c72fcb1cd25147157fdc2f8ad76de9a3fb6a",
   "wormhole-state-id": "0xaeab97f96cf9877fee2883315d459552b2b921edc16d7ceac6eab944dd88919c",
   "price-feed-to-price-info-object-table-id": "0x14b4697477d24c30c8eecc31dd1bd49a3115a9fe0db6bd4fd570cf14640b79a0",

+ 1 - 1
apps/price_pusher/config.ton.mainnet.sample.json

@@ -1,6 +1,6 @@
 {
   "endpoint": "https://toncenter.com/api/v2/jsonRPC",
-  "pyth-contract-address": "EQBU6k8HH6yX4Jf3d18swWbnYr31D3PJI7PgjXT",
+  "pyth-contract-address": "EQBU6k8HH6yX4Jf3d18swWbnYr31D3PJI7PgjXT-flsKHqql",
   "price-service-endpoint": "https://hermes.pyth.network",
   "private-key-file": "./mnemonic",
   "price-config-file": "./price-config.stable.sample.yaml"

+ 2 - 3
apps/price_pusher/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/price-pusher",
-  "version": "8.3.3",
+  "version": "9.0.0",
   "description": "Pyth Price Pusher",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",
@@ -24,7 +24,6 @@
     "format": "prettier --write \"src/**/*.ts\"",
     "test:lint": "eslint src/",
     "start": "node lib/index.js",
-    "test": "jest",
     "dev": "ts-node src/index.ts",
     "prepublishOnly": "pnpm run build && pnpm run test:lint",
     "preversion": "pnpm run test:lint",
@@ -61,7 +60,7 @@
     "@injectivelabs/networks": "^1.14.6",
     "@injectivelabs/sdk-ts": "1.10.72",
     "@mysten/sui": "^1.3.0",
-    "@pythnetwork/price-service-client": "workspace:*",
+    "@pythnetwork/hermes-client": "^1.3.1",
     "@pythnetwork/price-service-sdk": "workspace:^",
     "@pythnetwork/pyth-fuel-js": "workspace:*",
     "@pythnetwork/pyth-sdk-solidity": "workspace:*",

+ 5 - 0
apps/price_pusher/price-config.stable.sample.yaml

@@ -12,3 +12,8 @@
     time_difference: 30
     price_deviation: 0.5
     confidence_ratio: 0.1
+- alias: PYTH/USD
+  id: 2f95862b045670cd22bee3114c39763a4a08beeb663b145d283c31d7d1101c23
+  time_difference: 60
+  price_deviation: 0.5
+  confidence_ratio: 1

+ 0 - 101
apps/price_pusher/src/__tests__/pyth-price-listener.test.ts

@@ -1,101 +0,0 @@
-import { PythPriceListener } from "../pyth-price-listener";
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
-import { Logger } from "pino";
-
-describe("PythPriceListener", () => {
-  let logger: Logger;
-  let connection: PriceServiceConnection;
-  let listener: PythPriceListener;
-  let originalConsoleError: typeof console.error;
-
-  beforeEach(() => {
-    // Save original console.error and mock it
-    originalConsoleError = console.error;
-    console.error = jest.fn();
-
-    logger = {
-      debug: jest.fn(),
-      error: jest.fn(),
-      info: jest.fn(),
-    } as unknown as Logger;
-
-    // Use real Hermes beta endpoint for testing
-    connection = new PriceServiceConnection("https://hermes.pyth.network");
-  });
-
-  afterEach(() => {
-    // Clean up websocket connection
-    connection.closeWebSocket();
-    // Clean up health check interval
-    if (listener) {
-      listener.cleanup();
-    }
-    // Restore original console.error
-    console.error = originalConsoleError;
-  });
-
-  it("should handle invalid price feeds gracefully", async () => {
-    const validFeedId =
-      "e62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43"; // BTC/USD
-    const invalidFeedId =
-      "0000000000000000000000000000000000000000000000000000000000000000";
-
-    const priceItems = [
-      { id: validFeedId, alias: "BTC/USD" },
-      { id: invalidFeedId, alias: "INVALID/PRICE" },
-    ];
-
-    listener = new PythPriceListener(connection, priceItems, logger);
-
-    await listener.start();
-
-    // Wait for both error handlers to complete
-    await new Promise((resolve) => {
-      const checkInterval = setInterval(() => {
-        const errorCalls = (logger.error as jest.Mock).mock.calls;
-
-        // Check for both HTTP and websocket error logs
-        const hasHttpError = errorCalls.some(
-          (call) => call[0] === "Failed to get latest price feeds:"
-        );
-        const hasGetLatestError = errorCalls.some((call) =>
-          call[0].includes("not found for getLatestPriceFeeds")
-        );
-        const hasWsError = errorCalls.some((call) =>
-          call[0].includes("not found for subscribePriceFeedUpdates")
-        );
-
-        if (hasHttpError && hasGetLatestError && hasWsError) {
-          clearInterval(checkInterval);
-          resolve(true);
-        }
-      }, 100);
-    });
-
-    // Verify HTTP error was logged
-    expect(logger.error).toHaveBeenCalledWith(
-      "Failed to get latest price feeds:",
-      expect.objectContaining({
-        message: "Request failed with status code 404",
-      })
-    );
-
-    // Verify invalid feed error was logged
-    expect(logger.error).toHaveBeenCalledWith(
-      `Price feed ${invalidFeedId} (INVALID/PRICE) not found for getLatestPriceFeeds`
-    );
-
-    // Verify invalid feed error was logged
-    expect(logger.error).toHaveBeenCalledWith(
-      `Price feed ${invalidFeedId} (INVALID/PRICE) not found for subscribePriceFeedUpdates`
-    );
-
-    // Verify resubscription message was logged
-    expect(logger.info).toHaveBeenCalledWith(
-      "Resubscribing with valid feeds only"
-    );
-
-    // Verify priceIds was updated to only include valid feeds
-    expect(listener["priceIds"]).toEqual([validFeedId]);
-  });
-});

+ 8 - 7
apps/price_pusher/src/aptos/aptos.ts

@@ -6,7 +6,7 @@ import {
 } from "../interface";
 import { AptosAccount, AptosClient } from "aptos";
 import { DurationInSeconds } from "../utils";
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import { Logger } from "pino";
 
 export class AptosPriceListener extends ChainPriceListener {
@@ -89,7 +89,7 @@ export class AptosPricePusher implements IPricePusher {
   private sequenceNumberLocked: boolean;
 
   constructor(
-    private priceServiceConnection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private logger: Logger,
     private pythContractAddress: string,
     private endpoint: string,
@@ -107,11 +107,12 @@ export class AptosPricePusher implements IPricePusher {
    * @returns Array of price update data.
    */
   async getPriceFeedsUpdateData(priceIds: string[]): Promise<number[][]> {
-    // Fetch the latest price feed update VAAs from the price service
-    const latestVaas = await this.priceServiceConnection.getLatestVaas(
-      priceIds
+    const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
+      encoding: "base64",
+    });
+    return response.binary.data.map((data) =>
+      Array.from(Buffer.from(data, "base64"))
     );
-    return latestVaas.map((vaa) => Array.from(Buffer.from(vaa, "base64")));
   }
 
   async updatePriceFeed(
@@ -226,7 +227,7 @@ export class AptosPricePusher implements IPricePusher {
           );
           return this.lastSequenceNumber;
         } catch (e: any) {
-          throw new Error("Failed to retrieve sequence number");
+          throw new Error("Failed to retrieve sequence number" + e);
         } finally {
           this.sequenceNumberLocked = false;
         }

+ 21 - 17
apps/price_pusher/src/aptos/command.ts

@@ -1,4 +1,4 @@
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import * as options from "../options";
 import { readPriceConfigFile } from "../price-config";
 import fs from "fs";
@@ -12,7 +12,7 @@ import {
 } from "./aptos";
 import { AptosAccount } from "aptos";
 import pino from "pino";
-
+import { filterInvalidPriceItems } from "../utils";
 export default {
   command: "aptos",
   describe: "run price pusher for aptos",
@@ -39,10 +39,9 @@ export default {
     ...options.pollingFrequency,
     ...options.pushingFrequency,
     ...options.logLevel,
-    ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
-  handler: function (argv: any) {
+  handler: async function (argv: any) {
     // FIXME: type checks for this
     const {
       endpoint,
@@ -54,22 +53,13 @@ export default {
       pollingFrequency,
       overrideGasPriceMultiplier,
       logLevel,
-      priceServiceConnectionLogLevel,
       controllerLogLevel,
     } = argv;
 
     const logger = pino({ level: logLevel });
 
     const priceConfigs = readPriceConfigFile(priceConfigFile);
-    const priceServiceConnection = new PriceServiceConnection(
-      priceServiceEndpoint,
-      {
-        logger: logger.child(
-          { module: "PriceServiceConnection" },
-          { level: priceServiceConnectionLogLevel }
-        ),
-      }
-    );
+    const hermesClient = new HermesClient(priceServiceEndpoint);
 
     const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
     const account = AptosAccount.fromDerivePath(
@@ -78,10 +68,24 @@ export default {
     );
     logger.info(`Pushing from account address: ${account.address()}`);
 
-    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+    let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    // Better to filter out invalid price items before creating the pyth listener
+    const { existingPriceItems, invalidPriceItems } =
+      await filterInvalidPriceItems(hermesClient, priceItems);
+
+    if (invalidPriceItems.length > 0) {
+      logger.error(
+        `Invalid price id submitted for: ${invalidPriceItems
+          .map(({ alias }) => alias)
+          .join(", ")}`
+      );
+    }
+
+    priceItems = existingPriceItems;
 
     const pythListener = new PythPriceListener(
-      priceServiceConnection,
+      hermesClient,
       priceItems,
       logger.child({ module: "PythPriceListener" })
     );
@@ -95,7 +99,7 @@ export default {
     );
 
     const aptosPusher = new AptosPricePusher(
-      priceServiceConnection,
+      hermesClient,
       logger.child({ module: "AptosPricePusher" }),
       pythContractAddress,
       endpoint,

+ 1 - 1
apps/price_pusher/src/controller.ts

@@ -1,4 +1,4 @@
-import { UnixTimestamp } from "@pythnetwork/price-service-client";
+import { UnixTimestamp } from "@pythnetwork/hermes-client";
 import { DurationInSeconds, sleep } from "./utils";
 import { IPriceListener, IPricePusher } from "./interface";
 import { PriceConfig, shouldUpdate, UpdateCondition } from "./price-config";

+ 24 - 17
apps/price_pusher/src/evm/command.ts

@@ -1,4 +1,4 @@
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import fs from "fs";
 import { Options } from "yargs";
 import * as options from "../options";
@@ -10,7 +10,7 @@ import { getCustomGasStation } from "./custom-gas-station";
 import pino from "pino";
 import { createClient } from "./super-wallet";
 import { createPythContract } from "./pyth-contract";
-import { isWsEndpoint } from "../utils";
+import { isWsEndpoint, filterInvalidPriceItems } from "../utils";
 
 export default {
   command: "evm",
@@ -77,7 +77,6 @@ export default {
     ...options.pollingFrequency,
     ...options.pushingFrequency,
     ...options.logLevel,
-    ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
   handler: async function (argv: any) {
@@ -97,29 +96,37 @@ export default {
       gasLimit,
       updateFeeMultiplier,
       logLevel,
-      priceServiceConnectionLogLevel,
       controllerLogLevel,
     } = argv;
+    console.log("***** priceServiceEndpoint *****", priceServiceEndpoint);
 
-    const logger = pino({ level: logLevel });
+    const logger = pino({
+      level: logLevel,
+    });
 
     const priceConfigs = readPriceConfigFile(priceConfigFile);
-    const priceServiceConnection = new PriceServiceConnection(
-      priceServiceEndpoint,
-      {
-        logger: logger.child(
-          { module: "PriceServiceConnection" },
-          { level: priceServiceConnectionLogLevel }
-        ),
-      }
-    );
+    const hermesClient = new HermesClient(priceServiceEndpoint);
 
     const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
 
-    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+    let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    // Better to filter out invalid price items before creating the pyth listener
+    const { existingPriceItems, invalidPriceItems } =
+      await filterInvalidPriceItems(hermesClient, priceItems);
+
+    if (invalidPriceItems.length > 0) {
+      logger.error(
+        `Invalid price id submitted for: ${invalidPriceItems
+          .map(({ alias }) => alias)
+          .join(", ")}`
+      );
+    }
+
+    priceItems = existingPriceItems;
 
     const pythListener = new PythPriceListener(
-      priceServiceConnection,
+      hermesClient,
       priceItems,
       logger.child({ module: "PythPriceListener" })
     );
@@ -152,7 +159,7 @@ export default {
       txSpeed
     );
     const evmPusher = new EvmPricePusher(
-      priceServiceConnection,
+      hermesClient,
       client,
       pythContract,
       logger.child({ module: "EvmPricePusher" }),

+ 17 - 12
apps/price_pusher/src/evm/evm.ts

@@ -13,10 +13,10 @@ import {
 import { PythAbi } from "./pyth-abi";
 import { Logger } from "pino";
 import {
-  PriceServiceConnection,
+  HermesClient,
   HexString,
   UnixTimestamp,
-} from "@pythnetwork/price-service-client";
+} from "@pythnetwork/hermes-client";
 import { CustomGasStation } from "./custom-gas-station";
 import { PushAttempt } from "../common";
 import {
@@ -128,7 +128,7 @@ export class EvmPricePusher implements IPricePusher {
   private lastPushAttempt: PushAttempt | undefined;
 
   constructor(
-    private connection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private client: SuperWalletClient,
     private pythContract: PythContract,
     private logger: Logger,
@@ -156,17 +156,19 @@ export class EvmPricePusher implements IPricePusher {
     if (priceIds.length !== pubTimesToPush.length)
       throw new Error("Invalid arguments");
 
-    const priceIdsWith0x = priceIds.map((priceId) => addLeading0x(priceId));
-
     const priceFeedUpdateData = (await this.getPriceFeedsUpdateData(
-      priceIdsWith0x
+      priceIds
     )) as `0x${string}`[];
 
+    const priceFeedUpdateDataWith0x = priceFeedUpdateData.map((data) =>
+      addLeading0x(data)
+    );
+
     let updateFee;
 
     try {
       updateFee = await this.pythContract.read.getUpdateFee([
-        priceFeedUpdateData,
+        priceFeedUpdateDataWith0x,
       ]);
       updateFee = BigInt(
         Math.round(Number(updateFee) * (this.updateFeeMultiplier || 1))
@@ -227,10 +229,12 @@ export class EvmPricePusher implements IPricePusher {
       BigInt(pubTime)
     );
 
+    const priceIdsWith0x = priceIds.map((priceId) => addLeading0x(priceId));
+
     try {
       const { request } =
         await this.pythContract.simulate.updatePriceFeedsIfNecessary(
-          [priceFeedUpdateData, priceIdsWith0x, pubTimesToPushParam],
+          [priceFeedUpdateDataWith0x, priceIdsWith0x, pubTimesToPushParam],
           {
             value: updateFee,
             gasPrice: BigInt(Math.ceil(gasPrice)),
@@ -409,9 +413,10 @@ export class EvmPricePusher implements IPricePusher {
   private async getPriceFeedsUpdateData(
     priceIds: HexString[]
   ): Promise<string[]> {
-    const latestVaas = await this.connection.getLatestVaas(priceIds);
-    return latestVaas.map(
-      (vaa) => "0x" + Buffer.from(vaa, "base64").toString("hex")
-    );
+    const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
+      encoding: "hex",
+      ignoreInvalidPriceIds: true,
+    });
+    return response.binary.data;
   }
 }

+ 20 - 16
apps/price_pusher/src/fuel/command.ts

@@ -1,14 +1,14 @@
 import { Options } from "yargs";
 import * as options from "../options";
 import { readPriceConfigFile } from "../price-config";
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import { PythPriceListener } from "../pyth-price-listener";
 import { FuelPriceListener, FuelPricePusher } from "./fuel";
 import { Controller } from "../controller";
 import { Provider, Wallet } from "fuels";
 import fs from "fs";
 import pino from "pino";
-
+import { filterInvalidPriceItems } from "../utils";
 export default {
   command: "fuel",
   describe: "run price pusher for Fuel",
@@ -33,7 +33,6 @@ export default {
     ...options.pushingFrequency,
     ...options.pollingFrequency,
     ...options.logLevel,
-    ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
   handler: async function (argv: any) {
@@ -46,7 +45,6 @@ export default {
       pushingFrequency,
       pollingFrequency,
       logLevel,
-      priceServiceConnectionLogLevel,
       controllerLogLevel,
     } = argv;
 
@@ -54,20 +52,26 @@ export default {
 
     const priceConfigs = readPriceConfigFile(priceConfigFile);
 
-    const priceServiceConnection = new PriceServiceConnection(
-      priceServiceEndpoint,
-      {
-        logger: logger.child(
-          { module: "PriceServiceConnection" },
-          { level: priceServiceConnectionLogLevel }
-        ),
-      }
-    );
+    const hermesClient = new HermesClient(priceServiceEndpoint);
+
+    let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    // Better to filter out invalid price items before creating the pyth listener
+    const { existingPriceItems, invalidPriceItems } =
+      await filterInvalidPriceItems(hermesClient, priceItems);
+
+    if (invalidPriceItems.length > 0) {
+      logger.error(
+        `Invalid price id submitted for: ${invalidPriceItems
+          .map(({ alias }) => alias)
+          .join(", ")}`
+      );
+    }
 
-    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+    priceItems = existingPriceItems;
 
     const pythListener = new PythPriceListener(
-      priceServiceConnection,
+      hermesClient,
       priceItems,
       logger.child({ module: "PythPriceListener" })
     );
@@ -87,7 +91,7 @@ export default {
     const fuelPricePusher = new FuelPricePusher(
       wallet,
       pythContractAddress,
-      priceServiceConnection,
+      hermesClient,
       logger.child({ module: "FuelPricePusher" })
     );
 

+ 7 - 8
apps/price_pusher/src/fuel/fuel.ts

@@ -1,4 +1,4 @@
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import {
   ChainPriceListener,
   IPricePusher,
@@ -78,7 +78,7 @@ export class FuelPricePusher implements IPricePusher {
   constructor(
     private wallet: Wallet,
     private pythContractId: string,
-    private priceServiceConnection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private logger: Logger
   ) {
     this.contract = new Contract(
@@ -99,17 +99,16 @@ export class FuelPricePusher implements IPricePusher {
 
     let priceFeedUpdateData: string[];
     try {
-      priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
-        priceIds
-      );
+      const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
+        encoding: "base64",
+      });
+      priceFeedUpdateData = response.binary.data;
     } catch (err: any) {
       this.logger.error(err, "getPriceFeedsUpdateData failed");
       return;
     }
 
-    const updateData = priceFeedUpdateData.map((data) =>
-      arrayify(Buffer.from(data, "base64"))
-    );
+    const updateData = priceFeedUpdateData.map((data) => arrayify(data));
 
     try {
       const updateFee = await this.contract.functions

+ 21 - 17
apps/price_pusher/src/injective/command.ts

@@ -1,4 +1,4 @@
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import * as options from "../options";
 import { readPriceConfigFile } from "../price-config";
 import fs from "fs";
@@ -8,7 +8,7 @@ import { Controller } from "../controller";
 import { Options } from "yargs";
 import { getNetworkInfo } from "@injectivelabs/networks";
 import pino from "pino";
-
+import { filterInvalidPriceItems } from "../utils";
 export default {
   command: "injective",
   describe: "run price pusher for injective",
@@ -41,10 +41,9 @@ export default {
     ...options.pollingFrequency,
     ...options.pushingFrequency,
     ...options.logLevel,
-    ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
-  handler: function (argv: any) {
+  handler: async function (argv: any) {
     // FIXME: type checks for this
     const {
       gasPrice,
@@ -58,7 +57,6 @@ export default {
       pollingFrequency,
       network,
       logLevel,
-      priceServiceConnectionLogLevel,
       controllerLogLevel,
     } = argv;
 
@@ -69,21 +67,27 @@ export default {
     }
 
     const priceConfigs = readPriceConfigFile(priceConfigFile);
-    const priceServiceConnection = new PriceServiceConnection(
-      priceServiceEndpoint,
-      {
-        logger: logger.child(
-          { module: "PriceServiceConnection" },
-          { level: priceServiceConnectionLogLevel }
-        ),
-      }
-    );
+    const hermesClient = new HermesClient(priceServiceEndpoint);
     const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
 
-    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+    let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    // Better to filter out invalid price items before creating the pyth listener
+    const { existingPriceItems, invalidPriceItems } =
+      await filterInvalidPriceItems(hermesClient, priceItems);
+
+    if (invalidPriceItems.length > 0) {
+      logger.error(
+        `Invalid price id submitted for: ${invalidPriceItems
+          .map(({ alias }) => alias)
+          .join(", ")}`
+      );
+    }
+
+    priceItems = existingPriceItems;
 
     const pythListener = new PythPriceListener(
-      priceServiceConnection,
+      hermesClient,
       priceItems,
       logger.child({ module: "PythPriceListener" })
     );
@@ -98,7 +102,7 @@ export default {
       }
     );
     const injectivePusher = new InjectivePricePusher(
-      priceServiceConnection,
+      hermesClient,
       pythContractAddress,
       grpcEndpoint,
       logger.child({ module: "InjectivePricePusher" }),

+ 6 - 6
apps/price_pusher/src/injective/injective.ts

@@ -1,7 +1,4 @@
-import {
-  HexString,
-  PriceServiceConnection,
-} from "@pythnetwork/price-service-client";
+import { HexString, HermesClient } from "@pythnetwork/hermes-client";
 import {
   IPricePusher,
   PriceInfo,
@@ -100,7 +97,7 @@ export class InjectivePricePusher implements IPricePusher {
   private account: Account | null = null;
 
   constructor(
-    private priceServiceConnection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private pythContractAddress: string,
     private grpcEndpoint: string,
     private logger: Logger,
@@ -187,7 +184,10 @@ export class InjectivePricePusher implements IPricePusher {
   }
 
   async getPriceFeedUpdateObject(priceIds: string[]): Promise<any> {
-    const vaas = await this.priceServiceConnection.getLatestVaas(priceIds);
+    const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
+      encoding: "base64",
+    });
+    const vaas = response.binary.data;
 
     return {
       update_price_feeds: {

+ 1 - 1
apps/price_pusher/src/interface.ts

@@ -1,4 +1,4 @@
-import { HexString, UnixTimestamp } from "@pythnetwork/price-service-client";
+import { HexString, UnixTimestamp } from "@pythnetwork/hermes-client";
 import { DurationInSeconds } from "./utils";
 
 export type PriceItem = {

+ 21 - 16
apps/price_pusher/src/near/command.ts

@@ -1,4 +1,4 @@
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import * as options from "../options";
 import { readPriceConfigFile } from "../price-config";
 import { PythPriceListener } from "../pyth-price-listener";
@@ -6,6 +6,7 @@ import { Controller } from "../controller";
 import { Options } from "yargs";
 import { NearAccount, NearPriceListener, NearPricePusher } from "./near";
 import pino from "pino";
+import { filterInvalidPriceItems } from "../utils";
 
 export default {
   command: "near",
@@ -38,10 +39,9 @@ export default {
     ...options.pollingFrequency,
     ...options.pushingFrequency,
     ...options.logLevel,
-    ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
-  handler: function (argv: any) {
+  handler: async function (argv: any) {
     // FIXME: type checks for this
     const {
       nodeUrl,
@@ -54,27 +54,32 @@ export default {
       pushingFrequency,
       pollingFrequency,
       logLevel,
-      priceServiceConnectionLogLevel,
       controllerLogLevel,
     } = argv;
 
     const logger = pino({ level: logLevel });
 
     const priceConfigs = readPriceConfigFile(priceConfigFile);
-    const priceServiceConnection = new PriceServiceConnection(
-      priceServiceEndpoint,
-      {
-        logger: logger.child(
-          { module: "PriceServiceConnection" },
-          { level: priceServiceConnectionLogLevel }
-        ),
-      }
-    );
+    const hermesClient = new HermesClient(priceServiceEndpoint);
+
+    let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    // Better to filter out invalid price items before creating the pyth listener
+    const { existingPriceItems, invalidPriceItems } =
+      await filterInvalidPriceItems(hermesClient, priceItems);
+
+    if (invalidPriceItems.length > 0) {
+      logger.error(
+        `Invalid price id submitted for: ${invalidPriceItems
+          .map(({ alias }) => alias)
+          .join(", ")}`
+      );
+    }
 
-    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+    priceItems = existingPriceItems;
 
     const pythListener = new PythPriceListener(
-      priceServiceConnection,
+      hermesClient,
       priceItems,
       logger
     );
@@ -98,7 +103,7 @@ export default {
 
     const nearPusher = new NearPricePusher(
       nearAccount,
-      priceServiceConnection,
+      hermesClient,
       logger.child({ module: "NearPricePusher" })
     );
 

+ 6 - 7
apps/price_pusher/src/near/near.ts

@@ -8,10 +8,7 @@ import {
   ChainPriceListener,
   PriceItem,
 } from "../interface";
-import {
-  PriceServiceConnection,
-  HexString,
-} from "@pythnetwork/price-service-client";
+import { HermesClient, HexString } from "@pythnetwork/hermes-client";
 import { DurationInSeconds } from "../utils";
 
 import { Account, Connection, KeyPair } from "near-api-js";
@@ -64,7 +61,7 @@ export class NearPriceListener extends ChainPriceListener {
 export class NearPricePusher implements IPricePusher {
   constructor(
     private account: NearAccount,
-    private connection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private logger: Logger
   ) {}
 
@@ -132,8 +129,10 @@ export class NearPricePusher implements IPricePusher {
   private async getPriceFeedsUpdateData(
     priceIds: HexString[]
   ): Promise<string[]> {
-    const latestVaas = await this.connection.getLatestVaas(priceIds);
-    return latestVaas.map((vaa) => Buffer.from(vaa, "base64").toString("hex"));
+    const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
+      encoding: "base64",
+    });
+    return response.binary.data;
   }
 }
 

+ 1 - 11
apps/price_pusher/src/options.ts

@@ -3,7 +3,7 @@ import { Options } from "yargs";
 export const priceServiceEndpoint = {
   "price-service-endpoint": {
     description:
-      "Endpoint URL for the price service. e.g: https://endpoint/example",
+      "Endpoint URL for the hermes client. e.g: https://endpoint/example",
     type: "string",
     required: true,
   } as Options,
@@ -67,16 +67,6 @@ export const logLevel = {
   } as Options,
 };
 
-export const priceServiceConnectionLogLevel = {
-  "price-service-connection-log-level": {
-    description: "Log level for the price service connection.",
-    type: "string",
-    required: false,
-    default: "warn",
-    choices: ["trace", "debug", "info", "warn", "error"],
-  } as Options,
-};
-
 export const controllerLogLevel = {
   "controller-log-level": {
     description: "Log level for the controller.",

+ 1 - 1
apps/price_pusher/src/price-config.ts

@@ -1,4 +1,4 @@
-import { HexString } from "@pythnetwork/price-service-client";
+import { HexString } from "@pythnetwork/hermes-client";
 import Joi from "joi";
 import YAML from "yaml";
 import fs from "fs";

+ 42 - 126
apps/price_pusher/src/pyth-price-listener.ts

@@ -1,15 +1,15 @@
 import {
   HexString,
-  PriceFeed,
-  PriceServiceConnection,
-} from "@pythnetwork/price-service-client";
+  HermesClient,
+  PriceUpdate,
+} from "@pythnetwork/hermes-client";
 import { PriceInfo, IPriceListener, PriceItem } from "./interface";
 import { Logger } from "pino";
 
 type TimestampInMs = number & { readonly _: unique symbol };
 
 export class PythPriceListener implements IPriceListener {
-  private connection: PriceServiceConnection;
+  private hermesClient: HermesClient;
   private priceIds: HexString[];
   private priceIdToAlias: Map<HexString, string>;
   private latestPriceInfo: Map<HexString, PriceInfo>;
@@ -18,11 +18,11 @@ export class PythPriceListener implements IPriceListener {
   private healthCheckInterval?: NodeJS.Timeout;
 
   constructor(
-    connection: PriceServiceConnection,
+    hermesClient: HermesClient,
     priceItems: PriceItem[],
     logger: Logger
   ) {
-    this.connection = connection;
+    this.hermesClient = hermesClient;
     this.priceIds = priceItems.map((priceItem) => priceItem.id);
     this.priceIdToAlias = new Map(
       priceItems.map((priceItem) => [priceItem.id, priceItem.alias])
@@ -34,108 +34,47 @@ export class PythPriceListener implements IPriceListener {
   // This method should be awaited on and once it finishes it has the latest value
   // for the given price feeds (if they exist).
   async start() {
-    // Set custom error handler for websocket errors
-    this.connection.onWsError = (error: Error) => {
-      if (error.message.includes("not found")) {
-        // Extract invalid feed IDs from error message
-        const match = error.message.match(/\[(.*?)\]/);
-        if (match) {
-          const invalidFeedIds = match[1].split(",").map((id) => {
-            // Remove '0x' prefix if present to match our stored IDs
-            return id.trim().replace(/^0x/, "");
-          });
-
-          // Log invalid feeds with their aliases
-          invalidFeedIds.forEach((id) => {
-            this.logger.error(
-              `Price feed ${id} (${this.priceIdToAlias.get(
-                id
-              )}) not found for subscribePriceFeedUpdates`
-            );
-          });
-
-          // Filter out invalid feeds and resubscribe with valid ones
-          const validFeeds = this.priceIds.filter(
-            (id) => !invalidFeedIds.includes(id)
-          );
-
-          this.priceIds = validFeeds;
-
-          if (validFeeds.length > 0) {
-            this.logger.info("Resubscribing with valid feeds only");
-            this.connection.subscribePriceFeedUpdates(
-              validFeeds,
-              this.onNewPriceFeed.bind(this)
-            );
-          }
-        }
-      } else {
-        this.logger.error("Websocket error occurred:", error);
-      }
-    };
-
-    this.connection.subscribePriceFeedUpdates(
+    const eventSource = await this.hermesClient.getPriceUpdatesStream(
       this.priceIds,
-      this.onNewPriceFeed.bind(this)
+      {
+        parsed: true,
+        ignoreInvalidPriceIds: true,
+      }
     );
+    eventSource.onmessage = (event: MessageEvent<string>) => {
+      const priceUpdates = JSON.parse(event.data) as PriceUpdate;
+      priceUpdates.parsed?.forEach((priceUpdate) => {
+        this.logger.debug(
+          `Received new price feed update from Pyth price service: ${this.priceIdToAlias.get(
+            priceUpdate.id
+          )} ${priceUpdate.id}`
+        );
 
-    try {
-      const priceFeeds = await this.connection.getLatestPriceFeeds(
-        this.priceIds
-      );
-      priceFeeds?.forEach((priceFeed) => {
-        const latestAvailablePrice = priceFeed.getPriceUnchecked();
-        this.latestPriceInfo.set(priceFeed.id, {
-          price: latestAvailablePrice.price,
-          conf: latestAvailablePrice.conf,
-          publishTime: latestAvailablePrice.publishTime,
-        });
-      });
-    } catch (error: any) {
-      // Always log the HTTP error first
-      this.logger.error("Failed to get latest price feeds:", error);
-
-      if (error.response.data.includes("Price ids not found:")) {
-        // Extract invalid feed IDs from error message
-        const invalidFeedIds = error.response.data
-          .split("Price ids not found:")[1]
-          .split(",")
-          .map((id: string) => id.trim().replace(/^0x/, ""));
-
-        // Log invalid feeds with their aliases
-        invalidFeedIds.forEach((id: string) => {
-          this.logger.error(
-            `Price feed ${id} (${this.priceIdToAlias.get(
-              id
-            )}) not found for getLatestPriceFeeds`
-          );
-        });
+        // Consider price to be currently available if it is not older than 60s
+        const currentPrice =
+          Date.now() / 1000 - priceUpdate.price.publish_time > 60
+            ? undefined
+            : priceUpdate.price;
+        if (currentPrice === undefined) {
+          this.logger.debug("Price is older than 60s, skipping");
+          return;
+        }
 
-        // Filter out invalid feeds and retry
-        const validFeeds = this.priceIds.filter(
-          (id) => !invalidFeedIds.includes(id)
-        );
+        const priceInfo: PriceInfo = {
+          conf: currentPrice.conf,
+          price: currentPrice.price,
+          publishTime: currentPrice.publish_time,
+        };
 
-        this.priceIds = validFeeds;
+        this.latestPriceInfo.set(priceUpdate.id, priceInfo);
+        this.lastUpdated = Date.now() as TimestampInMs;
+      });
+    };
 
-        if (validFeeds.length > 0) {
-          this.logger.info(
-            "Retrying getLatestPriceFeeds with valid feeds only"
-          );
-          const validPriceFeeds = await this.connection.getLatestPriceFeeds(
-            validFeeds
-          );
-          validPriceFeeds?.forEach((priceFeed) => {
-            const latestAvailablePrice = priceFeed.getPriceUnchecked();
-            this.latestPriceInfo.set(priceFeed.id, {
-              price: latestAvailablePrice.price,
-              conf: latestAvailablePrice.conf,
-              publishTime: latestAvailablePrice.publishTime,
-            });
-          });
-        }
-      }
-    }
+    eventSource.onerror = (error: Event) => {
+      console.error("Error receiving updates from Hermes:", error);
+      eventSource.close();
+    };
 
     // Store health check interval reference
     this.healthCheckInterval = setInterval(() => {
@@ -148,30 +87,7 @@ export class PythPriceListener implements IPriceListener {
     }, 5000);
   }
 
-  private onNewPriceFeed(priceFeed: PriceFeed) {
-    this.logger.debug(
-      `Received new price feed update from Pyth price service: ${this.priceIdToAlias.get(
-        priceFeed.id
-      )} ${priceFeed.id}`
-    );
-
-    // Consider price to be currently available if it is not older than 60s
-    const currentPrice = priceFeed.getPriceNoOlderThan(60);
-    if (currentPrice === undefined) {
-      return;
-    }
-
-    const priceInfo: PriceInfo = {
-      conf: currentPrice.conf,
-      price: currentPrice.price,
-      publishTime: currentPrice.publishTime,
-    };
-
-    this.latestPriceInfo.set(priceFeed.id, priceInfo);
-    this.lastUpdated = Date.now() as TimestampInMs;
-  }
-
-  getLatestPriceInfo(priceId: string): PriceInfo | undefined {
+  getLatestPriceInfo(priceId: HexString): PriceInfo | undefined {
     return this.latestPriceInfo.get(priceId);
   }
 

+ 22 - 18
apps/price_pusher/src/solana/command.ts

@@ -1,7 +1,6 @@
 import { Options } from "yargs";
 import * as options from "../options";
 import { readPriceConfigFile } from "../price-config";
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
 import { PythPriceListener } from "../pyth-price-listener";
 import {
   SolanaPriceListener,
@@ -20,7 +19,8 @@ import {
 } from "jito-ts/dist/sdk/block-engine/searcher";
 import pino from "pino";
 import { Logger } from "pino";
-
+import { HermesClient } from "@pythnetwork/hermes-client";
+import { filterInvalidPriceItems } from "../utils";
 export default {
   command: "solana",
   describe: "run price pusher for solana",
@@ -87,10 +87,9 @@ export default {
     ...options.pollingFrequency,
     ...options.pushingFrequency,
     ...options.logLevel,
-    ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
-  handler: function (argv: any) {
+  handler: async function (argv: any) {
     const {
       endpoint,
       keypairFile,
@@ -109,7 +108,6 @@ export default {
       jitoBundleSize,
       updatesPerJitoBundle,
       logLevel,
-      priceServiceConnectionLogLevel,
       controllerLogLevel,
     } = argv;
 
@@ -117,20 +115,26 @@ export default {
 
     const priceConfigs = readPriceConfigFile(priceConfigFile);
 
-    const priceServiceConnection = new PriceServiceConnection(
-      priceServiceEndpoint,
-      {
-        logger: logger.child(
-          { module: "PriceServiceConnection" },
-          { level: priceServiceConnectionLogLevel }
-        ),
-      }
-    );
+    const hermesClient = new HermesClient(priceServiceEndpoint);
+
+    let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    // Better to filter out invalid price items before creating the pyth listener
+    const { existingPriceItems, invalidPriceItems } =
+      await filterInvalidPriceItems(hermesClient, priceItems);
+
+    if (invalidPriceItems.length > 0) {
+      logger.error(
+        `Invalid price id submitted for: ${invalidPriceItems
+          .map(({ alias }) => alias)
+          .join(", ")}`
+      );
+    }
 
-    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+    priceItems = existingPriceItems;
 
     const pythListener = new PythPriceListener(
-      priceServiceConnection,
+      hermesClient,
       priceItems,
       logger.child({ module: "PythPriceListener" })
     );
@@ -156,7 +160,7 @@ export default {
       const jitoClient = searcherClient(jitoEndpoint, jitoKeypair);
       solanaPricePusher = new SolanaPricePusherJito(
         pythSolanaReceiver,
-        priceServiceConnection,
+        hermesClient,
         logger.child({ module: "SolanaPricePusherJito" }),
         shardId,
         jitoTipLamports,
@@ -171,7 +175,7 @@ export default {
     } else {
       solanaPricePusher = new SolanaPricePusher(
         pythSolanaReceiver,
-        priceServiceConnection,
+        hermesClient,
         logger.child({ module: "SolanaPricePusher" }),
         shardId,
         computeUnitPriceMicroLamports

+ 13 - 8
apps/price_pusher/src/solana/solana.ts

@@ -6,7 +6,7 @@ import {
   PriceItem,
 } from "../interface";
 import { DurationInSeconds } from "../utils";
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import {
   sendTransactions,
   sendTransactionsJito,
@@ -94,7 +94,7 @@ export class SolanaPriceListener extends ChainPriceListener {
 export class SolanaPricePusher implements IPricePusher {
   constructor(
     private pythSolanaReceiver: PythSolanaReceiver,
-    private priceServiceConnection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private logger: Logger,
     private shardId: number,
     private computeUnitPriceMicroLamports: number
@@ -114,9 +114,13 @@ export class SolanaPricePusher implements IPricePusher {
 
     let priceFeedUpdateData;
     try {
-      priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
-        shuffledPriceIds
+      const response = await this.hermesClient.getLatestPriceUpdates(
+        shuffledPriceIds,
+        {
+          encoding: "base64",
+        }
       );
+      priceFeedUpdateData = response.binary.data;
     } catch (err: any) {
       this.logger.error(err, "getPriceFeedsUpdateData failed:");
       return;
@@ -152,7 +156,7 @@ export class SolanaPricePusher implements IPricePusher {
 export class SolanaPricePusherJito implements IPricePusher {
   constructor(
     private pythSolanaReceiver: PythSolanaReceiver,
-    private priceServiceConnection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private logger: Logger,
     private shardId: number,
     private defaultJitoTipLamports: number,
@@ -201,9 +205,10 @@ export class SolanaPricePusherJito implements IPricePusher {
 
     let priceFeedUpdateData: string[];
     try {
-      priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
-        priceIds
-      );
+      const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
+        encoding: "base64",
+      });
+      priceFeedUpdateData = response.binary.data;
     } catch (err: any) {
       this.logger.error(err, "getPriceFeedsUpdateData failed");
       return;

+ 22 - 19
apps/price_pusher/src/sui/command.ts

@@ -1,4 +1,4 @@
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import * as options from "../options";
 import { readPriceConfigFile } from "../price-config";
 import fs from "fs";
@@ -8,13 +8,14 @@ import { Options } from "yargs";
 import { SuiPriceListener, SuiPricePusher } from "./sui";
 import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
 import pino from "pino";
+import { filterInvalidPriceItems } from "../utils";
 
 export default {
   command: "sui",
   describe:
     "Run price pusher for sui. Most of the arguments below are" +
     "network specific, so there's one set of values for mainnet and" +
-    "another for testnet. See config.sui..sample.json for the " +
+    " another for testnet. See config.sui.mainnet.sample.json for the " +
     "appropriate values for your network. ",
   builder: {
     endpoint: {
@@ -70,7 +71,6 @@ export default {
     ...options.pollingFrequency,
     ...options.pushingFrequency,
     ...options.logLevel,
-    ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
   handler: async function (argv: any) {
@@ -88,25 +88,14 @@ export default {
       gasBudget,
       accountIndex,
       logLevel,
-      priceServiceConnectionLogLevel,
       controllerLogLevel,
     } = argv;
 
     const logger = pino({ level: logLevel });
 
     const priceConfigs = readPriceConfigFile(priceConfigFile);
-    const priceServiceConnection = new PriceServiceConnection(
-      priceServiceEndpoint,
-      {
-        logger: logger.child(
-          { module: "PriceServiceConnection" },
-          { level: priceServiceConnectionLogLevel }
-        ),
-        priceFeedRequestConfig: {
-          binary: true,
-        },
-      }
-    );
+    const hermesClient = new HermesClient(priceServiceEndpoint);
+
     const mnemonic = fs.readFileSync(mnemonicFile, "utf-8").trim();
     const keypair = Ed25519Keypair.deriveKeypair(
       mnemonic,
@@ -118,10 +107,24 @@ export default {
         .toSuiAddress()}`
     );
 
-    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+    let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    // Better to filter out invalid price items before creating the pyth listener
+    const { existingPriceItems, invalidPriceItems } =
+      await filterInvalidPriceItems(hermesClient, priceItems);
+
+    if (invalidPriceItems.length > 0) {
+      logger.error(
+        `Invalid price id submitted for: ${invalidPriceItems
+          .map(({ alias }) => alias)
+          .join(", ")}`
+      );
+    }
+
+    priceItems = existingPriceItems;
 
     const pythListener = new PythPriceListener(
-      priceServiceConnection,
+      hermesClient,
       priceItems,
       logger.child({ module: "PythPriceListener" })
     );
@@ -135,7 +138,7 @@ export default {
       { pollingFrequency }
     );
     const suiPusher = await SuiPricePusher.createWithAutomaticGasPool(
-      priceServiceConnection,
+      hermesClient,
       logger.child({ module: "SuiPricePusher" }),
       pythStateId,
       wormholeStateId,

+ 12 - 10
apps/price_pusher/src/sui/sui.ts

@@ -5,13 +5,12 @@ import {
   PriceItem,
 } from "../interface";
 import { DurationInSeconds } from "../utils";
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
 import { SuiPythClient } from "@pythnetwork/pyth-sui-js";
 import { Ed25519Keypair } from "@mysten/sui/keypairs/ed25519";
 import { Transaction } from "@mysten/sui/transactions";
 import { SuiClient, SuiObjectRef, PaginatedCoins } from "@mysten/sui/client";
 import { Logger } from "pino";
-
+import { HermesClient } from "@pythnetwork/hermes-client";
 const GAS_FEE_FOR_SPLIT = 2_000_000_000;
 // TODO: read this from on chain config
 const MAX_NUM_GAS_OBJECTS_IN_PTB = 256;
@@ -111,7 +110,7 @@ export class SuiPricePusher implements IPricePusher {
     private readonly signer: Ed25519Keypair,
     private readonly provider: SuiClient,
     private logger: Logger,
-    private priceServiceConnection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private gasBudget: number,
     private gasPool: SuiObjectRef[],
     private pythClient: SuiPythClient
@@ -157,7 +156,7 @@ export class SuiPricePusher implements IPricePusher {
    * The gas coins of the wallet for the provided keypair will be merged and then evenly split into `numGasObjects`.
    */
   static async createWithAutomaticGasPool(
-    priceServiceConnection: PriceServiceConnection,
+    hermesClient: HermesClient,
     logger: Logger,
     pythStateId: string,
     wormholeStateId: string,
@@ -193,7 +192,7 @@ export class SuiPricePusher implements IPricePusher {
       keypair,
       provider,
       logger,
-      priceServiceConnection,
+      hermesClient,
       gasBudget,
       gasPool,
       pythClient
@@ -223,15 +222,18 @@ export class SuiPricePusher implements IPricePusher {
 
     await Promise.all(
       priceIdChunks.map(async (priceIdChunk) => {
-        const vaas = await this.priceServiceConnection.getLatestVaas(
-          priceIdChunk
+        const response = await this.hermesClient.getLatestPriceUpdates(
+          priceIdChunk,
+          {
+            encoding: "base64",
+          }
         );
-        if (vaas.length !== 1) {
+        if (response.binary.data.length !== 1) {
           throw new Error(
-            `Expected a single VAA for all priceIds ${priceIdChunk} but received ${vaas.length} VAAs: ${vaas}`
+            `Expected a single VAA for all priceIds ${priceIdChunk} but received ${response.binary.data.length} VAAs: ${response.binary.data}`
           );
         }
-        const vaa = vaas[0];
+        const vaa = response.binary.data[0];
         const tx = new Transaction();
         await this.pythClient.updatePriceFeeds(
           tx,

+ 20 - 15
apps/price_pusher/src/ton/command.ts

@@ -1,13 +1,14 @@
 import { Options } from "yargs";
 import * as options from "../options";
 import { readPriceConfigFile } from "../price-config";
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
 import { PythPriceListener } from "../pyth-price-listener";
 import { TonPriceListener, TonPricePusher } from "./ton";
 import { Controller } from "../controller";
 import { Address, TonClient } from "@ton/ton";
 import fs from "fs";
 import pino from "pino";
+import { HermesClient } from "@pythnetwork/hermes-client";
+import { filterInvalidPriceItems } from "../utils";
 
 export default {
   command: "ton",
@@ -33,7 +34,6 @@ export default {
     ...options.pushingFrequency,
     ...options.pollingFrequency,
     ...options.logLevel,
-    ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
   handler: async function (argv: any) {
@@ -46,7 +46,6 @@ export default {
       pushingFrequency,
       pollingFrequency,
       logLevel,
-      priceServiceConnectionLogLevel,
       controllerLogLevel,
     } = argv;
 
@@ -54,20 +53,26 @@ export default {
 
     const priceConfigs = readPriceConfigFile(priceConfigFile);
 
-    const priceServiceConnection = new PriceServiceConnection(
-      priceServiceEndpoint,
-      {
-        logger: logger.child(
-          { module: "PriceServiceConnection" },
-          { level: priceServiceConnectionLogLevel }
-        ),
-      }
-    );
+    const hermesClient = new HermesClient(priceServiceEndpoint);
+
+    let priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    // Better to filter out invalid price items before creating the pyth listener
+    const { existingPriceItems, invalidPriceItems } =
+      await filterInvalidPriceItems(hermesClient, priceItems);
+
+    if (invalidPriceItems.length > 0) {
+      logger.error(
+        `Invalid price id submitted for: ${invalidPriceItems
+          .map(({ alias }) => alias)
+          .join(", ")}`
+      );
+    }
 
-    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+    priceItems = existingPriceItems;
 
     const pythListener = new PythPriceListener(
-      priceServiceConnection,
+      hermesClient,
       priceItems,
       logger.child({ module: "PythPriceListener" })
     );
@@ -89,7 +94,7 @@ export default {
       client,
       privateKey,
       contractAddress,
-      priceServiceConnection,
+      hermesClient,
       logger.child({ module: "TonPricePusher" })
     );
 

+ 6 - 5
apps/price_pusher/src/ton/ton.ts

@@ -1,4 +1,4 @@
-import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { HermesClient } from "@pythnetwork/hermes-client";
 import {
   ChainPriceListener,
   IPricePusher,
@@ -70,7 +70,7 @@ export class TonPricePusher implements IPricePusher {
     private client: TonClient,
     private privateKey: string,
     private contractAddress: Address,
-    private priceServiceConnection: PriceServiceConnection,
+    private hermesClient: HermesClient,
     private logger: Logger
   ) {
     this.contract = this.client
@@ -96,9 +96,10 @@ export class TonPricePusher implements IPricePusher {
 
     let priceFeedUpdateData: string[];
     try {
-      priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
-        priceIds
-      );
+      const response = await this.hermesClient.getLatestPriceUpdates(priceIds, {
+        encoding: "base64",
+      });
+      priceFeedUpdateData = response.binary.data;
     } catch (err: any) {
       this.logger.error(err, "getPriceFeedsUpdateData failed");
       return;

+ 31 - 1
apps/price_pusher/src/utils.ts

@@ -1,4 +1,5 @@
-import { HexString } from "@pythnetwork/price-service-client";
+import { HermesClient, HexString } from "@pythnetwork/hermes-client";
+import { PriceItem } from "./interface";
 
 export type PctNumber = number;
 export type DurationInSeconds = number;
@@ -54,3 +55,32 @@ export const assertDefined = <T>(value: T | undefined): T => {
     return value;
   }
 };
+
+export async function filterInvalidPriceItems(
+  hermesClient: HermesClient,
+  priceItems: PriceItem[]
+): Promise<{
+  existingPriceItems: PriceItem[];
+  invalidPriceItems: PriceItem[];
+}> {
+  const priceMetadata = await hermesClient.getPriceFeeds();
+  const allPriceIds = priceMetadata.map((priceMetadata) => priceMetadata.id);
+
+  // Filter out invalid price ids
+  const { existingPriceItems, invalidPriceItems } = priceItems.reduce<{
+    existingPriceItems: PriceItem[];
+    invalidPriceItems: PriceItem[];
+  }>(
+    (acc, item) => {
+      if (allPriceIds.includes(item.id)) {
+        acc.existingPriceItems.push(item);
+      } else {
+        acc.invalidPriceItems.push(item);
+      }
+      return acc;
+    },
+    { existingPriceItems: [], invalidPriceItems: [] }
+  );
+
+  return { existingPriceItems, invalidPriceItems };
+}

+ 356 - 11
pnpm-lock.yaml

@@ -659,9 +659,9 @@ importers:
       '@mysten/sui':
         specifier: ^1.3.0
         version: 1.3.0(svelte@4.2.18)(typescript@5.5.4)
-      '@pythnetwork/price-service-client':
-        specifier: workspace:*
-        version: link:../../price_service/client/js
+      '@pythnetwork/hermes-client':
+        specifier: ^1.3.1
+        version: 1.3.1(axios@1.7.7)
       '@pythnetwork/price-service-sdk':
         specifier: workspace:^
         version: link:../../price_service/sdk/js
@@ -2592,7 +2592,7 @@ importers:
     dependencies:
       '@certusone/wormhole-sdk':
         specifier: ^0.9.12
-        version: 0.9.24(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@5.0.10)
+        version: 0.9.24(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@6.0.4)
       '@mysten/sui':
         specifier: ^1.3.0
         version: 1.3.0(svelte@4.2.18)(typescript@5.5.4)
@@ -2601,7 +2601,7 @@ importers:
         version: link:../../../contract_manager
       '@pythnetwork/price-service-client':
         specifier: ^1.4.0
-        version: 1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+        version: 1.9.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       '@pythnetwork/price-service-sdk':
         specifier: ^1.2.0
         version: 1.7.1
@@ -6262,6 +6262,9 @@ packages:
     peerDependencies:
       '@solana/web3.js': 1.92.3
 
+  '@pythnetwork/hermes-client@1.3.1':
+    resolution: {integrity: sha512-iJq4Surv9TKEwMIdhSnOxSdBSCfyUR+J4MPOvoFm2EisQBGBhDHIwDM4wPfbd/hBUiUcvVQoWWqeiSlznJ3iPQ==}
+
   '@pythnetwork/price-service-client@1.9.0':
     resolution: {integrity: sha512-SLm3IFcfmy9iMqHeT4Ih6qMNZhJEefY14T9yTlpsH2D/FE5+BaGGnfcexUifVlfH6M7mwRC4hEFdNvZ6ebZjJg==}
     deprecated: This package is deprecated and is no longer maintained. Please use @pythnetwork/hermes-client instead.
@@ -15372,6 +15375,7 @@ packages:
 
   lodash.isequal@4.5.0:
     resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
+    deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
 
   lodash.ismatch@4.4.0:
     resolution: {integrity: sha512-fPMfXjGQEV9Xsq/8MTSgUf255gawYRbjwMyDbcvDhXgV7enSZA0hynz6vMPnpAb5iONEzBHBPsT+0zes5Z301g==}
@@ -22431,6 +22435,41 @@ snapshots:
       - subscriptions-transport-ws
       - utf-8-validate
 
+  '@certusone/wormhole-sdk@0.9.24(bufferutil@4.0.8)(encoding@0.1.13)(google-protobuf@3.21.4)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@certusone/wormhole-sdk-proto-web': 0.0.6(google-protobuf@3.21.4)
+      '@certusone/wormhole-sdk-wasm': 0.0.1
+      '@coral-xyz/borsh': 0.2.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))
+      '@mysten/sui.js': 0.32.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@project-serum/anchor': 0.25.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      '@solana/spl-token': 0.3.7(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      '@terra-money/terra.js': 3.1.9
+      '@xpla/xpla.js': 0.2.3
+      algosdk: 2.7.0
+      aptos: 1.5.0
+      axios: 0.24.0
+      bech32: 2.0.0
+      binary-parser: 2.2.1
+      bs58: 4.0.1
+      elliptic: 6.5.6
+      js-base64: 3.7.5
+      near-api-js: 1.1.0(encoding@0.1.13)
+    optionalDependencies:
+      '@injectivelabs/networks': 1.10.12(google-protobuf@3.21.4)
+      '@injectivelabs/sdk-ts': 1.10.72(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@injectivelabs/utils': 1.10.12(google-protobuf@3.21.4)
+    transitivePeerDependencies:
+      - bufferutil
+      - debug
+      - encoding
+      - google-protobuf
+      - graphql-ws
+      - react
+      - react-dom
+      - subscriptions-transport-ws
+      - utf-8-validate
+
   '@chain-registry/types@0.28.1': {}
 
   '@chain-registry/types@0.43.10': {}
@@ -22577,6 +22616,12 @@ snapshots:
       bn.js: 5.2.1
       buffer-layout: 1.2.2
 
+  '@coral-xyz/borsh@0.2.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))':
+    dependencies:
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      bn.js: 5.2.1
+      buffer-layout: 1.2.2
+
   '@coral-xyz/borsh@0.27.0(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))':
     dependencies:
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -22750,6 +22795,17 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  '@cosmjs/socket@0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@cosmjs/stream': 0.30.1
+      isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      xstream: 11.14.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+    optional: true
+
   '@cosmjs/socket@0.32.3(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
     dependencies:
       '@cosmjs/stream': 0.32.3
@@ -22799,6 +22855,26 @@ snapshots:
       - debug
       - utf-8-validate
 
+  '@cosmjs/stargate@0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@confio/ics23': 0.6.8
+      '@cosmjs/amino': 0.30.1
+      '@cosmjs/encoding': 0.30.1
+      '@cosmjs/math': 0.30.1
+      '@cosmjs/proto-signing': 0.30.1
+      '@cosmjs/stream': 0.30.1
+      '@cosmjs/tendermint-rpc': 0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@cosmjs/utils': 0.30.1
+      cosmjs-types: 0.7.2
+      long: 4.0.0
+      protobufjs: 6.11.4
+      xstream: 11.14.0
+    transitivePeerDependencies:
+      - bufferutil
+      - debug
+      - utf-8-validate
+    optional: true
+
   '@cosmjs/stargate@0.32.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
     dependencies:
       '@confio/ics23': 0.6.8
@@ -22876,6 +22952,24 @@ snapshots:
       - debug
       - utf-8-validate
 
+  '@cosmjs/tendermint-rpc@0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@cosmjs/crypto': 0.30.1
+      '@cosmjs/encoding': 0.30.1
+      '@cosmjs/json-rpc': 0.30.1
+      '@cosmjs/math': 0.30.1
+      '@cosmjs/socket': 0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@cosmjs/stream': 0.30.1
+      '@cosmjs/utils': 0.30.1
+      axios: 0.21.4(debug@4.3.7)
+      readonly-date: 1.0.0
+      xstream: 11.14.0
+    transitivePeerDependencies:
+      - bufferutil
+      - debug
+      - utf-8-validate
+    optional: true
+
   '@cosmjs/tendermint-rpc@0.32.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
     dependencies:
       '@cosmjs/crypto': 0.32.3
@@ -23806,6 +23900,33 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  '@ethersproject/providers@5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    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(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+    optional: true
+
   '@ethersproject/random@5.7.0':
     dependencies:
       '@ethersproject/bytes': 5.7.0
@@ -24749,6 +24870,54 @@ snapshots:
       - subscriptions-transport-ws
       - utf-8-validate
 
+  '@injectivelabs/sdk-ts@1.10.72(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@apollo/client': 3.7.13(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+      '@cosmjs/amino': 0.30.1
+      '@cosmjs/proto-signing': 0.30.1
+      '@cosmjs/stargate': 0.30.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@ethersproject/bytes': 5.7.0
+      '@injectivelabs/core-proto-ts': 0.0.14
+      '@injectivelabs/exceptions': 1.14.6(google-protobuf@3.21.4)
+      '@injectivelabs/grpc-web': 0.0.1(google-protobuf@3.21.4)
+      '@injectivelabs/grpc-web-node-http-transport': 0.0.2(@injectivelabs/grpc-web@0.0.1(google-protobuf@3.21.4))
+      '@injectivelabs/grpc-web-react-native-transport': 0.0.2(@injectivelabs/grpc-web@0.0.1(google-protobuf@3.21.4))
+      '@injectivelabs/indexer-proto-ts': 1.10.8-rc.4
+      '@injectivelabs/mito-proto-ts': 1.0.9
+      '@injectivelabs/networks': 1.14.6(google-protobuf@3.21.4)
+      '@injectivelabs/test-utils': 1.14.4
+      '@injectivelabs/token-metadata': 1.10.42(google-protobuf@3.21.4)
+      '@injectivelabs/ts-types': 1.14.6
+      '@injectivelabs/utils': 1.14.6(google-protobuf@3.21.4)
+      '@metamask/eth-sig-util': 4.0.1
+      axios: 0.27.2
+      bech32: 2.0.0
+      bip39: 3.0.4
+      cosmjs-types: 0.7.2
+      eth-crypto: 2.6.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      ethereumjs-util: 7.1.5
+      ethers: 5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      google-protobuf: 3.21.4
+      graphql: 16.9.0
+      http-status-codes: 2.2.0
+      js-sha3: 0.8.0
+      jscrypto: 1.0.3
+      keccak256: 1.0.6
+      link-module-alias: 1.2.0
+      rxjs: 7.8.1
+      secp256k1: 4.0.3
+      shx: 0.3.4
+      snakecase-keys: 5.4.5
+    transitivePeerDependencies:
+      - bufferutil
+      - debug
+      - graphql-ws
+      - react
+      - react-dom
+      - subscriptions-transport-ws
+      - utf-8-validate
+    optional: true
+
   '@injectivelabs/sdk-ts@1.14.7(bufferutil@4.0.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(utf-8-validate@5.0.10)':
     dependencies:
       '@apollo/client': 3.7.13(graphql@16.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -26135,6 +26304,22 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  '@mysten/sui.js@0.32.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@mysten/bcs': 0.7.1
+      '@noble/curves': 1.7.0
+      '@noble/hashes': 1.6.1
+      '@scure/bip32': 1.6.0
+      '@scure/bip39': 1.5.0
+      '@suchipi/femver': 1.0.0
+      jayson: 4.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      rpc-websockets: 7.5.1
+      superstruct: 1.0.4
+      tweetnacl: 1.0.3
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
   '@mysten/sui@1.3.0(svelte@4.2.18)(typescript@5.5.4)':
     dependencies:
       '@graphql-typed-document-node/core': 3.2.0(graphql@16.9.0)
@@ -27095,6 +27280,28 @@ snapshots:
       - encoding
       - utf-8-validate
 
+  '@project-serum/anchor@0.25.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@project-serum/borsh': 0.2.5(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      base64-js: 1.5.1
+      bn.js: 5.2.1
+      bs58: 4.0.1
+      buffer-layout: 1.2.2
+      camelcase: 5.3.1
+      cross-fetch: 3.1.8(encoding@0.1.13)
+      crypto-hash: 1.3.0
+      eventemitter3: 4.0.7
+      js-sha256: 0.9.0
+      pako: 2.1.0
+      snake-case: 3.0.4
+      superstruct: 0.15.5
+      toml: 3.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
   '@project-serum/borsh@0.2.5(@solana/web3.js@1.92.3(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@6.0.3))':
     dependencies:
       '@solana/web3.js': 1.92.3(bufferutil@4.0.7)(encoding@0.1.13)(utf-8-validate@6.0.3)
@@ -27107,6 +27314,12 @@ snapshots:
       bn.js: 5.2.1
       buffer-layout: 1.2.2
 
+  '@project-serum/borsh@0.2.5(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))':
+    dependencies:
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      bn.js: 5.2.1
+      buffer-layout: 1.2.2
+
   '@project-serum/sol-wallet-adapter@0.2.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))':
     dependencies:
       '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
@@ -27158,15 +27371,23 @@ snapshots:
       - encoding
       - utf-8-validate
 
-  '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
+  '@pythnetwork/hermes-client@1.3.1(axios@1.7.7)':
+    dependencies:
+      '@zodios/core': 10.9.6(axios@1.7.7)(zod@3.23.8)
+      eventsource: 2.0.2
+      zod: 3.23.8
+    transitivePeerDependencies:
+      - axios
+
+  '@pythnetwork/price-service-client@1.9.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
     dependencies:
       '@pythnetwork/price-service-sdk': 1.7.1
       '@types/ws': 8.5.13
       axios: 1.7.7(debug@4.3.7)
       axios-retry: 3.9.1
-      isomorphic-ws: 4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10))
+      isomorphic-ws: 4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4))
       ts-log: 2.2.7
-      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
     transitivePeerDependencies:
       - bufferutil
       - debug
@@ -29582,6 +29803,17 @@ snapshots:
       - encoding
       - utf-8-validate
 
+  '@solana/buffer-layout-utils@0.2.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      bigint-buffer: 1.1.5
+      bignumber.js: 9.1.2
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
   '@solana/buffer-layout@4.0.1':
     dependencies:
       buffer: 6.0.3
@@ -29681,6 +29913,17 @@ snapshots:
       - encoding
       - utf-8-validate
 
+  '@solana/spl-token@0.3.7(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4))(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@solana/buffer-layout': 4.0.1
+      '@solana/buffer-layout-utils': 0.2.0(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      '@solana/web3.js': 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)
+      buffer: 6.0.3
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
   '@solana/spl-token@0.4.6(@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10))(bufferutil@4.0.8)(encoding@0.1.13)(fastestsmallesttextencoderdecoder@1.0.22)(utf-8-validate@5.0.10)':
     dependencies:
       '@solana/buffer-layout': 4.0.1
@@ -30402,6 +30645,28 @@ snapshots:
       - encoding
       - utf-8-validate
 
+  '@solana/web3.js@1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@6.0.4)':
+    dependencies:
+      '@babel/runtime': 7.25.7
+      '@noble/curves': 1.7.0
+      '@noble/hashes': 1.6.1
+      '@solana/buffer-layout': 4.0.1
+      agentkeepalive: 4.5.0
+      bigint-buffer: 1.1.5
+      bn.js: 5.2.1
+      borsh: 0.7.0
+      bs58: 4.0.1
+      buffer: 6.0.3
+      fast-stable-stringify: 1.0.0
+      jayson: 4.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      node-fetch: 2.7.0(encoding@0.1.13)
+      rpc-websockets: 8.0.1
+      superstruct: 1.0.4
+    transitivePeerDependencies:
+      - bufferutil
+      - encoding
+      - utf-8-validate
+
   '@solana/web3.js@1.92.3(encoding@0.1.13)':
     dependencies:
       '@babel/runtime': 7.25.7
@@ -30415,7 +30680,7 @@ snapshots:
       bs58: 4.0.1
       buffer: 6.0.3
       fast-stable-stringify: 1.0.0
-      jayson: 4.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      jayson: 4.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       node-fetch: 2.7.0(encoding@0.1.13)
       rpc-websockets: 8.0.1
       superstruct: 1.0.4
@@ -38111,6 +38376,20 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  eth-crypto@2.6.0(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    dependencies:
+      '@babel/runtime': 7.20.13
+      '@ethereumjs/tx': 3.5.2
+      '@types/bn.js': 5.1.1
+      eccrypto: 1.1.6(patch_hash=rjcfmtfgn3z72mudpdif5oxmye)
+      ethereumjs-util: 7.1.5
+      ethers: 5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      secp256k1: 5.0.0
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+    optional: true
+
   eth-ens-namehash@2.0.8:
     dependencies:
       idna-uts46-hx: 2.3.1
@@ -38429,6 +38708,43 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  ethers@5.7.2(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    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(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+      '@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
+    optional: true
+
   ethers@6.13.4(bufferutil@4.0.7)(utf-8-validate@6.0.3):
     dependencies:
       '@adraffy/ens-normalize': 1.10.1
@@ -40377,9 +40693,9 @@ snapshots:
     dependencies:
       ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@5.0.10)
 
-  isomorphic-ws@4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)):
+  isomorphic-ws@4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)):
     dependencies:
-      ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)
 
   isomorphic-ws@4.0.1(ws@8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)):
     dependencies:
@@ -40529,6 +40845,24 @@ snapshots:
       - bufferutil
       - utf-8-validate
 
+  jayson@4.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    dependencies:
+      '@types/connect': 3.4.38
+      '@types/node': 12.20.55
+      '@types/ws': 7.4.7
+      JSONStream: 1.3.5
+      commander: 2.20.3
+      delay: 5.0.0
+      es6-promisify: 5.0.0
+      eyes: 0.1.8
+      isomorphic-ws: 4.0.1(ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+      json-stringify-safe: 5.0.1
+      uuid: 8.3.2
+      ws: 7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+
   jest-changed-files@29.7.0:
     dependencies:
       execa: 5.1.1
@@ -50086,6 +50420,12 @@ snapshots:
       bufferutil: 4.0.8
       utf-8-validate: 5.0.10
 
+  ws@7.4.6(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 6.0.4
+    optional: true
+
   ws@7.5.10(bufferutil@4.0.7)(utf-8-validate@6.0.3):
     optionalDependencies:
       bufferutil: 4.0.7
@@ -50096,6 +50436,11 @@ snapshots:
       bufferutil: 4.0.8
       utf-8-validate: 5.0.10
 
+  ws@7.5.10(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+    optionalDependencies:
+      bufferutil: 4.0.8
+      utf-8-validate: 6.0.4
+
   ws@8.11.0(bufferutil@4.0.8)(utf-8-validate@5.0.10):
     optionalDependencies:
       bufferutil: 4.0.8