diff --git a/apps/price_pusher/README.md b/apps/price_pusher/README.md index 653a313dff..f9eaa41d8c 100644 --- a/apps/price_pusher/README.md +++ b/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 diff --git a/apps/price_pusher/config.aptos.testnet.sample.json b/apps/price_pusher/config.aptos.testnet.sample.json index 6fa695b447..88f449ead3 100644 --- a/apps/price_pusher/config.aptos.testnet.sample.json +++ b/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", diff --git a/apps/price_pusher/config.evm.stable.sample.json b/apps/price_pusher/config.evm.mainnet.sample.json similarity index 100% rename from apps/price_pusher/config.evm.stable.sample.json rename to apps/price_pusher/config.evm.mainnet.sample.json diff --git a/apps/price_pusher/config.injective.testnet.sample.json b/apps/price_pusher/config.injective.testnet.sample.json index 3745730953..e28cca964a 100644 --- a/apps/price_pusher/config.injective.testnet.sample.json +++ b/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", diff --git a/apps/price_pusher/config.sui.mainnet.sample.json b/apps/price_pusher/config.sui.mainnet.sample.json index 2a3750862b..10cfa11d10 100644 --- a/apps/price_pusher/config.sui.mainnet.sample.json +++ b/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", diff --git a/apps/price_pusher/config.ton.mainnet.sample.json b/apps/price_pusher/config.ton.mainnet.sample.json index feebcf5dd3..2f59d0c817 100644 --- a/apps/price_pusher/config.ton.mainnet.sample.json +++ b/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" diff --git a/apps/price_pusher/package.json b/apps/price_pusher/package.json index 4e144e2733..3a858803f3 100644 --- a/apps/price_pusher/package.json +++ b/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:*", diff --git a/apps/price_pusher/price-config.stable.sample.yaml b/apps/price_pusher/price-config.stable.sample.yaml index dd47b48a50..3f33e19d00 100644 --- a/apps/price_pusher/price-config.stable.sample.yaml +++ b/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 diff --git a/apps/price_pusher/src/__tests__/pyth-price-listener.test.ts b/apps/price_pusher/src/__tests__/pyth-price-listener.test.ts deleted file mode 100644 index c0ece74181..0000000000 --- a/apps/price_pusher/src/__tests__/pyth-price-listener.test.ts +++ /dev/null @@ -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]); - }); -}); diff --git a/apps/price_pusher/src/aptos/aptos.ts b/apps/price_pusher/src/aptos/aptos.ts index 612b2eeb28..6341dfbe96 100644 --- a/apps/price_pusher/src/aptos/aptos.ts +++ b/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 { - // 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; } diff --git a/apps/price_pusher/src/aptos/command.ts b/apps/price_pusher/src/aptos/command.ts index a5e6e89a1a..74a3adf0c0 100644 --- a/apps/price_pusher/src/aptos/command.ts +++ b/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, diff --git a/apps/price_pusher/src/controller.ts b/apps/price_pusher/src/controller.ts index 1632b0e92f..2788066e1c 100644 --- a/apps/price_pusher/src/controller.ts +++ b/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"; diff --git a/apps/price_pusher/src/evm/command.ts b/apps/price_pusher/src/evm/command.ts index 58126a92d9..37bd5daede 100644 --- a/apps/price_pusher/src/evm/command.ts +++ b/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" }), diff --git a/apps/price_pusher/src/evm/evm.ts b/apps/price_pusher/src/evm/evm.ts index 24fe283d76..7101a1792c 100644 --- a/apps/price_pusher/src/evm/evm.ts +++ b/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 { - 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; } } diff --git a/apps/price_pusher/src/fuel/command.ts b/apps/price_pusher/src/fuel/command.ts index 518dc31729..1f344e79e4 100644 --- a/apps/price_pusher/src/fuel/command.ts +++ b/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" }) ); diff --git a/apps/price_pusher/src/fuel/fuel.ts b/apps/price_pusher/src/fuel/fuel.ts index 1a650a71e1..25e7a71e5e 100644 --- a/apps/price_pusher/src/fuel/fuel.ts +++ b/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 diff --git a/apps/price_pusher/src/injective/command.ts b/apps/price_pusher/src/injective/command.ts index ea397f64a3..a73d200cfb 100644 --- a/apps/price_pusher/src/injective/command.ts +++ b/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" }), diff --git a/apps/price_pusher/src/injective/injective.ts b/apps/price_pusher/src/injective/injective.ts index 16c45a4b53..e6d5c367a2 100644 --- a/apps/price_pusher/src/injective/injective.ts +++ b/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 { - 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: { diff --git a/apps/price_pusher/src/interface.ts b/apps/price_pusher/src/interface.ts index a126da7025..a09ddb7927 100644 --- a/apps/price_pusher/src/interface.ts +++ b/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 = { diff --git a/apps/price_pusher/src/near/command.ts b/apps/price_pusher/src/near/command.ts index da93c14cfe..b4f5aaf7d4 100644 --- a/apps/price_pusher/src/near/command.ts +++ b/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" }) ); diff --git a/apps/price_pusher/src/near/near.ts b/apps/price_pusher/src/near/near.ts index d805ff38c2..ffc1e77053 100644 --- a/apps/price_pusher/src/near/near.ts +++ b/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 { - 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; } } diff --git a/apps/price_pusher/src/options.ts b/apps/price_pusher/src/options.ts index 95c6497389..1c09bf0f32 100644 --- a/apps/price_pusher/src/options.ts +++ b/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.", diff --git a/apps/price_pusher/src/price-config.ts b/apps/price_pusher/src/price-config.ts index 76a6cbc6ac..94bcb59b3d 100644 --- a/apps/price_pusher/src/price-config.ts +++ b/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"; diff --git a/apps/price_pusher/src/pyth-price-listener.ts b/apps/price_pusher/src/pyth-price-listener.ts index 24a35a5d85..f54a1a0bfd 100644 --- a/apps/price_pusher/src/pyth-price-listener.ts +++ b/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; private latestPriceInfo: Map; @@ -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) => { + 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); } diff --git a/apps/price_pusher/src/solana/command.ts b/apps/price_pusher/src/solana/command.ts index 8d4e1dcb3d..55ddd42b08 100644 --- a/apps/price_pusher/src/solana/command.ts +++ b/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 diff --git a/apps/price_pusher/src/solana/solana.ts b/apps/price_pusher/src/solana/solana.ts index 30604c8c2b..668c46c128 100644 --- a/apps/price_pusher/src/solana/solana.ts +++ b/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; diff --git a/apps/price_pusher/src/sui/command.ts b/apps/price_pusher/src/sui/command.ts index 41f6b663a7..3b869d6207 100644 --- a/apps/price_pusher/src/sui/command.ts +++ b/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, diff --git a/apps/price_pusher/src/sui/sui.ts b/apps/price_pusher/src/sui/sui.ts index eeca6be448..b3cd53ac3f 100644 --- a/apps/price_pusher/src/sui/sui.ts +++ b/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, diff --git a/apps/price_pusher/src/ton/command.ts b/apps/price_pusher/src/ton/command.ts index defd0103f2..17ed6439d4 100644 --- a/apps/price_pusher/src/ton/command.ts +++ b/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" }) ); diff --git a/apps/price_pusher/src/ton/ton.ts b/apps/price_pusher/src/ton/ton.ts index a9851e51ee..af9e14a140 100644 --- a/apps/price_pusher/src/ton/ton.ts +++ b/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; diff --git a/apps/price_pusher/src/utils.ts b/apps/price_pusher/src/utils.ts index d540acbfa5..4c87a6c157 100644 --- a/apps/price_pusher/src/utils.ts +++ b/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 = (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 }; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0255849ccf..490ba4e7e3 100644 --- a/pnpm-lock.yaml +++ b/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