Ver código fonte

feat(evm/sdk/js): add pyth filler (#2832)

* chore: remove obsolete files

* feat(evm/sdk/js): add pyth filler

* refactor: address comments and fix issues

* chore: bump version
Ali Behjati 3 meses atrás
pai
commit
1ae21393f5

Diferenças do arquivo suprimidas por serem muito extensas
+ 129 - 232
pnpm-lock.yaml


+ 0 - 10
target_chains/ethereum/sdk/js/.eslintrc.js

@@ -1,10 +0,0 @@
-module.exports = {
-  root: true,
-  parser: "@typescript-eslint/parser",
-  plugins: ["@typescript-eslint"],
-  extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
-  rules: {
-    "@typescript-eslint/no-explicit-any": "off",
-    "@typescript-eslint/ban-ts-comment": "off",
-  },
-};

+ 49 - 149
target_chains/ethereum/sdk/js/README.md

@@ -1,7 +1,4 @@
-# Pyth EVM JS (DEPRECATED)
-
-> [!WARNING]
-> **DEPRECATION NOTICE:** This package is deprecated and no longer maintained. Please use [hermes-client](https://github.com/pyth-network/pyth-crosschain/tree/main/apps/hermes/client/js) instead.
+# Pyth EVM JS
 
 [Pyth](https://pyth.network/) provides real-time pricing data in a variety of asset classes, including cryptocurrency,
 equities, FX and commodities. This library allows you to use these real-time prices on EVM-based networks.
@@ -22,156 +19,59 @@ $ yarn add @pythnetwork/pyth-evm-js
 
 ## Quickstart
 
-Pyth stores prices off-chain to minimize gas fees, which allows us to offer a wider selection of products and faster
-update times. See [On-Demand Updates](https://docs.pyth.network/documentation/pythnet-price-feeds/on-demand) for more
-information about this approach. In order to use Pyth prices on chain, they must be fetched from an off-chain Hermes
-instance. The `EvmPriceServiceConnection` class can be used to interact with these services, providing a way to fetch
-these prices directly in your code. The following example wraps an existing RPC provider and shows how to obtain Pyth
-prices and submit them to the network:
-
-```typescript
-const connection = new EvmPriceServiceConnection("https://hermes.pyth.network"); // See Hermes endpoints section below for other endpoints
-
-const priceIds = [
-  // You can find the ids of prices at https://pyth.network/developers/price-feed-ids#pyth-evm-stable
-  "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43", // BTC/USD price id
-  "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace", // ETH/USD price id
-];
-
-// In order to use Pyth prices in your protocol you need to submit the price update data to Pyth contract in your target
-// chain. `getPriceFeedsUpdateData` creates the update data which can be submitted to your contract. Then your contract should
-// call the Pyth Contract with this data.
-const priceUpdateData = await connection.getPriceFeedsUpdateData(priceIds);
-
-// If the user is paying the price update fee, you need to fetch it from the Pyth contract.
-// Please refer to https://docs.pyth.network/documentation/pythnet-price-feeds/on-demand#fees for more information.
-//
-// `pythContract` below is a web3.js contract; if you wish to use ethers, you need to change it accordingly.
-// You can find the Pyth interface ABI in @pythnetwork/pyth-sdk-solidity npm package.
-const updateFee = await pythContract.methods
-  .getUpdateFee(priceUpdateData)
-  .call();
-
-// Calling someContract method
-// `someContract` below is a web3.js contract; if you wish to use ethers, you need to change it accordingly.
-// Note: In Hedera you need to pass updateFee * 10^10 as value to the send method as there is an
-// inconsistency in the way the value is handled in Hedera RPC and the way it is handled on-chain.
-await someContract.methods
-  .doSomething(someArg, otherArg, priceUpdateData)
-  .send({ value: updateFee });
-```
-
-`SomeContract` looks like so:
-
-```solidity
-pragma solidity ^0.8.0;
-
-import "@pythnetwork/pyth-sdk-solidity/IPyth.sol";
-import "@pythnetwork/pyth-sdk-solidity/PythStructs.sol";
-
-contract SomeContract {
-  IPyth pyth;
-
-  constructor(address pythContract) {
-    pyth = IPyth(pythContract);
-  }
+### Filling Pyth Data for Transactions
 
-  function doSomething(
-    uint someArg,
-    string memory otherArg,
-    bytes[] calldata priceUpdateData
-  ) public payable {
-    // Update the prices to be set to the latest values
-    uint fee = pyth.getUpdateFee(priceUpdateData);
-    pyth.updatePriceFeeds{ value: fee }(priceUpdateData);
-
-    // Doing other things that uses prices
-    bytes32 priceId = 0xf9c0172ba10dfa4d19088d94f5bf61d3b54d5bd7483a322a982e1373ee8ea31b;
-    // Get the price if it is not older than 10 seconds from the current time.
-    PythStructs.Price price = pyth.getPriceNoOlderThan(priceId, 10);
-  }
-}
-
-```
-
-We strongly recommend reading our guide which explains [how to work with Pyth price feeds](https://docs.pyth.network/documentation/pythnet-price-feeds/best-practices).
-
-### Off-chain prices
-
-Many applications additionally need to display Pyth prices off-chain, for example, in their frontend application.
-The `EvmPriceServiceConnection` provides two different ways to fetch the current Pyth price.
-The code blocks below assume that the `connection` and `priceIds` objects have been initialized as shown above.
-The first method is a single-shot query:
+The `fillPythUpdate` function helps you automatically determine what Pyth price updates are needed for a transaction and creates the necessary update call.
+This function uses the `trace_callMany` method by default but can be used with `debug_traceCall` and a bundler as well. See the example below for more information.
 
 ```typescript
-// `getLatestPriceFeeds` returns a `PriceFeed` for each price id. It contains all information about a price and has
-// utility functions to get the current and exponentially-weighted moving average price, and other functionality.
-const priceFeeds = await connection.getLatestPriceFeeds(priceIds);
-// Get the price if it is not older than 60 seconds from the current time.
-console.log(priceFeeds[0].getPriceNoOlderThan(60)); // Price { conf: '1234', expo: -8, price: '12345678' }
-// Get the exponentially-weighted moving average price if it is not older than 60 seconds from the current time.
-console.log(priceFeeds[1].getEmaPriceNoOlderThan(60));
-```
+import { fillPythUpdate, multicall3Bundler, CallRequest } from "@pythnetwork/pyth-evm-js";
+import { createPublicClient, http } from "viem";
+import { optimismSepolia } from "viem/chains";
 
-The object also supports a streaming websocket connection that allows you to subscribe to every new price update for a given feed.
-This method is useful if you want to show continuously updating real-time prices in your frontend:
+const PYTH_CONTRACT_OP_SEPOLIA = "0x0708325268df9f66270f1401206434524814508b"
+const HERMES_ENDPOINT = "https://hermes.pyth.network"
 
-```typescript
-// Subscribe to the price feeds given by `priceId`. The callback will be invoked every time the requested feed
-// gets a price update.
-connection.subscribePriceFeedUpdates(priceIds, (priceFeed) => {
-  console.log(
-    `Received update for ${priceFeed.id}: ${priceFeed.getPriceNoOlderThan(60)}`
-  );
+const client = createPublicClient({
+    chain: optimismSepolia,
+    transport: http("YOUR_RPC_ENDPOINT"),
 });
 
-// When using the subscription, make sure to close the websocket upon termination to finish the process gracefully.
-setTimeout(() => {
-  connection.closeWebSocket();
-}, 60000);
-```
-
-### Examples
-
-There are two examples in [examples](./src/examples/).
-
-#### EvmPriceServiceClient
-
-[This example](./src/examples/EvmPriceServiceClient.ts) fetches `PriceFeed` updates using both a HTTP-request API and a streaming websocket API. You can run it with `npm run example-client`. A full command that prints BTC and ETH price feeds, in the testnet network, looks like so:
-
-```bash
-npm run example-client -- \
-  --endpoint https://hermes.pyth.network \
-  --price-ids \
-    0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43 \
-    0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace
-```
-
-#### EvmRelay
-
-[This example](./src/examples/EvmRelay.ts) shows how to update prices on an EVM network. It does the following:
-
-1. Gets update data to update given price feeds.
-2. Calls the pyth contract with the update data.
-3. Submits it to the network and prints the txhash if successful.
-
-You can run this example with `npm run example-relay`. A full command that updates BTC and ETH prices on the BNB Chain
-testnet network looks like so:
-
-```bash
-npm run example-relay -- \
-  --network "https://data-seed-prebsc-1-s1.binance.org:8545" \
-  --pyth-contract "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb"\
-  --mnemonic "my good mnemonic" \
-  --endpoint https://hermes.pyth.network \
-  --price-ids \
-    "0xe62df6c8b4a85fe1a67db44dc12de5db330f7ac66b72dc658afedf0f4a415b43" \
-    "0xff61491a931112ddf1bd8147cd1b641375f79f5825126d665480874634fd0ace"
+const call: CallRequest = {
+    to: "0x3252c2F7962689fA17f892C52555613f36056f22",
+    data: "0xd09de08a", // Your transaction calldata
+    from: "0x78357316239040e19fC823372cC179ca75e64b81",
+};
+
+// Fill Pyth update data using "trace_callMany"
+const pythUpdate = await fillPythUpdate(
+    client,
+    call,
+    PYTH_CONTRACT_OP_SEPOLIA,
+    HERMES_ENDPOINT,
+    {
+        method: "trace_callMany",
+        maxIter: 5,
+    },
+);
+
+// Fill Pyth update data using "debug_traceCall"
+const _pythUpdateWithDebugTraceCall = await fillPythUpdate(
+    client,
+    call,
+    PYTH_CONTRACT_OP_SEPOLIA,
+    HERMES_ENDPOINT,
+    {
+        method: "debug_traceCall",
+        bundler: multicall3Bundler, // or any function that takes a PythUpdate and a CallRequest and produces a CallRequest
+        maxIter: 5,
+    },
+);
+
+if (pythUpdate) {
+    console.log("Pyth update needed:", pythUpdate);
+    // Bundle the calls together, or pass the pythUpdate.updateData to your contract.
+} else {
+    console.log("No Pyth data needed for this transaction");
+}
 ```
-
-## Hermes endpoints
-
-Pyth offers a free public endpoint at [https://hermes.pyth.network](https://hermes.pyth.network). However, it is
-recommended to obtain a private endpoint from one of the Hermes RPC providers for more reliability. You can find more
-information about Hermes RPC providers
-[here](https://docs.pyth.network/documentation/pythnet-price-feeds/hermes#public-endpoint).

+ 1 - 0
target_chains/ethereum/sdk/js/eslint.config.js

@@ -0,0 +1 @@
+export { base as default } from "@cprussin/eslint-config";

+ 7 - 13
target_chains/ethereum/sdk/js/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-evm-js",
-  "version": "1.83.0",
+  "version": "2.0.0-alpha2",
   "description": "Pyth Network EVM Utils in JS",
   "homepage": "https://pyth.network",
   "author": {
@@ -21,16 +21,13 @@
   },
   "scripts": {
     "build": "tsc",
-    "example-client": "pnpm run build && node lib/examples/EvmPriceServiceClient.js",
-    "example-relay": "pnpm run build && node lib/examples/EvmRelay.js",
-    "example-benchmark": "pnpm run build && node lib/examples/EvmBenchmark.js",
     "test:format": "prettier --check \"src/**/*.ts\"",
     "test:lint": "eslint src/ --max-warnings 0",
     "fix:format": "prettier --write \"src/**/*.ts\"",
     "fix:lint": "eslint src/ --fix --max-warnings 0",
     "prepublishOnly": "pnpm run build && pnpm run test:lint",
     "preversion": "pnpm run test:lint",
-    "version": "pnpm run format && git add -A src"
+    "version": "pnpm run test:format && git add -A src"
   },
   "keywords": [
     "pyth",
@@ -38,6 +35,7 @@
   ],
   "license": "Apache-2.0",
   "devDependencies": {
+    "@cprussin/eslint-config": "catalog:",
     "@pythnetwork/pyth-sdk-solidity": "workspace:*",
     "@truffle/hdwallet-provider": "^2.1.5",
     "@types/ethereum-protocol": "^1.0.2",
@@ -45,18 +43,14 @@
     "@types/node": "^18.11.18",
     "@types/web3-provider-engine": "^14.0.1",
     "@types/yargs": "^17.0.10",
-    "@typescript-eslint/eslint-plugin": "^5.21.0",
-    "@typescript-eslint/parser": "^5.21.0",
-    "eslint": "^8.14.0",
+    "eslint": "catalog:",
     "jest": "^29.4.1",
     "prettier": "catalog:",
     "ts-jest": "^29.0.5",
-    "typescript": "^4.6.3",
-    "web3": "^1.8.2",
-    "yargs": "^17.4.1"
+    "typescript": "catalog:"
   },
   "dependencies": {
-    "@pythnetwork/price-service-client": "workspace:*",
-    "buffer": "^6.0.3"
+    "@pythnetwork/hermes-client": "workspace:*",
+    "viem": "catalog:"
   }
 }

+ 0 - 21
target_chains/ethereum/sdk/js/src/EvmPriceServiceConnection.ts

@@ -1,21 +0,0 @@
-import {
-  PriceServiceConnection,
-  HexString,
-} from "@pythnetwork/price-service-client";
-import { Buffer } from "buffer";
-
-export class EvmPriceServiceConnection extends PriceServiceConnection {
-  /**
-   * Gets price update data which then can be submitted to Pyth contract to update the prices.
-   * This will throw an axios error if there is a network problem or the price service returns a non-ok response (e.g: Invalid price ids)
-   *
-   * @param priceIds Array of hex-encoded price ids.
-   * @returns Array of price update data.
-   */
-  async getPriceFeedsUpdateData(priceIds: HexString[]): Promise<string[]> {
-    const latestVaas = await this.getLatestVaas(priceIds);
-    return latestVaas.map(
-      (vaa) => "0x" + Buffer.from(vaa, "base64").toString("hex"),
-    );
-  }
-}

+ 0 - 117
target_chains/ethereum/sdk/js/src/examples/EvmBenchmark.ts

@@ -1,117 +0,0 @@
-import Web3 from "web3";
-import yargs from "yargs";
-import { hideBin } from "yargs/helpers";
-
-import { EvmPriceServiceConnection } from "../index";
-import HDWalletProvider from "@truffle/hdwallet-provider";
-import PythInterfaceAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
-
-const argv = yargs(hideBin(process.argv))
-  .option("network", {
-    description: "RPC of the network to relay on.",
-    type: "string",
-    required: true,
-  })
-  .option("endpoint", {
-    description:
-      "Endpoint URL for the price service. e.g: https://endpoint/example",
-    type: "string",
-    required: true,
-  })
-  .option("pyth-contract", {
-    description: "Pyth contract address.",
-    type: "string",
-    required: true,
-  })
-  .option("price-id", {
-    description:
-      "Price feed id (in hex) to fetch" + " e.g: 0xf9c0172ba10dfa4d19088d...",
-    type: "string",
-    required: true,
-  })
-  .option("timestamp", {
-    description: "Timestamp of the prices to fetch" + " e.g., 2022-", // TODO
-    type: "string",
-    required: true,
-  })
-  .option("mnemonic", {
-    description: "Mnemonic (private key) for sender",
-    type: "string",
-    required: true,
-  })
-  .help()
-  .alias("help", "h")
-  .parserConfiguration({
-    "parse-numbers": false,
-  })
-  .parseSync();
-
-const network = argv.network;
-const pythContractAddr = argv.pythContract;
-
-const connection = new EvmPriceServiceConnection(argv.endpoint);
-
-async function run() {
-  const provider = new HDWalletProvider({
-    mnemonic: {
-      phrase: argv.mnemonic,
-    },
-    providerOrUrl: network,
-  });
-
-  // @ts-ignore
-  const web3 = new Web3(provider);
-  const priceId = argv.priceId as string;
-  // The unix timestamp in seconds
-  const unixTimestamp = Date.parse(argv.timestamp) / 1000;
-
-  console.log(`Querying unix timestamp: ${unixTimestamp}`);
-
-  const [priceFeedUpdateVaa, updateTimestamp] = await connection.getVaa(
-    priceId,
-    unixTimestamp,
-  );
-  console.log(`Next pyth update was at: ${updateTimestamp}`);
-  console.log(priceFeedUpdateVaa);
-
-  const priceFeedUpdate =
-    "0x" + Buffer.from(priceFeedUpdateVaa, "base64").toString("hex");
-
-  const pythContract = new web3.eth.Contract(
-    PythInterfaceAbi as any,
-    pythContractAddr,
-    {
-      from: provider.getAddress(0),
-    },
-  );
-
-  const updateFee = await pythContract.methods
-    .getUpdateFee([priceFeedUpdate])
-    .call();
-  console.log(`Update fee: ${updateFee}`);
-
-  // In real use cases, you would pass the update to your contract, then call parsePriceFeedUpdates within your contract.
-  // When invoked on-chain, this function will return a PriceFeed struct containing the data in the price update
-  // (such as the current price).
-  await pythContract.methods
-    .parsePriceFeedUpdates(
-      [priceFeedUpdate],
-      [priceId],
-      // parsePriceFeedUpdates will reject any price update outside of the time window provided in the following
-      // two arguments. Integrators can use this to specify the timestamp of the update they are expecting.
-      unixTimestamp,
-      unixTimestamp + 5,
-    )
-    .send({ value: updateFee })
-    .on("transactionHash", (hash: string) => {
-      console.log(`Tx hash: ${hash}`);
-    })
-    .on("error", (err: any, receipt: any) => {
-      console.error(receipt);
-      throw err;
-    });
-
-  provider.engine.stop();
-}
-
-run();

+ 0 - 64
target_chains/ethereum/sdk/js/src/examples/EvmPriceServiceClient.ts

@@ -1,64 +0,0 @@
-import yargs from "yargs";
-import { hideBin } from "yargs/helpers";
-
-import { EvmPriceServiceConnection } from "../index";
-
-function sleep(ms: number) {
-  return new Promise((resolve) => setTimeout(resolve, ms));
-}
-
-const argv = yargs(hideBin(process.argv))
-  .option("endpoint", {
-    description:
-      "Endpoint URL for the Price Service. e.g: https://endpoint/example",
-    type: "string",
-    required: true,
-  })
-  .option("price-ids", {
-    description:
-      "Space separated price feed ids (in hex) to fetch" +
-      " e.g: 0xf9c0172ba10dfa4d19088d...",
-    type: "array",
-    required: true,
-  })
-  .help()
-  .alias("help", "h")
-  .parserConfiguration({
-    "parse-numbers": false,
-  })
-  .parseSync();
-
-async function run() {
-  const connection = new EvmPriceServiceConnection(argv.endpoint, {
-    logger: console, // Providing logger will allow the connection to log its events.
-  });
-
-  const priceIds = argv.priceIds as string[];
-  console.log(priceIds);
-  const priceFeeds = await connection.getLatestPriceFeeds(priceIds);
-  console.log(priceFeeds);
-  console.log(priceFeeds?.at(0)?.getPriceNoOlderThan(60));
-
-  const updateData = await connection.getPriceFeedsUpdateData(priceIds);
-  console.log(updateData);
-
-  console.log("Subscribing to price feed updates.");
-
-  await connection.subscribePriceFeedUpdates(priceIds, (priceFeed) => {
-    console.log(
-      `Current price for ${priceFeed.id}: ${JSON.stringify(
-        priceFeed.getPriceNoOlderThan(60),
-      )}.`,
-    );
-  });
-
-  await sleep(600000);
-
-  // To close the websocket you should either unsubscribe from all
-  // price feeds or call `connection.closeWebSocket()` directly.
-
-  console.log("Unsubscribing from price feed updates.");
-  await connection.unsubscribePriceFeedUpdates(priceIds);
-}
-
-run();

+ 0 - 120
target_chains/ethereum/sdk/js/src/examples/EvmRelay.ts

@@ -1,120 +0,0 @@
-import Web3 from "web3";
-import yargs from "yargs";
-import { hideBin } from "yargs/helpers";
-
-import { EvmPriceServiceConnection } from "../index";
-import HDWalletProvider from "@truffle/hdwallet-provider";
-import PythInterfaceAbi from "@pythnetwork/pyth-sdk-solidity/abis/IPyth.json";
-
-const argv = yargs(hideBin(process.argv))
-  .option("network", {
-    description: "RPC of the network to relay on.",
-    type: "string",
-    required: true,
-  })
-  .option("endpoint", {
-    description:
-      "Endpoint URL for the price service. e.g: https://endpoint/example",
-    type: "string",
-    required: true,
-  })
-  .option("pyth-contract", {
-    description: "Pyth contract address.",
-    type: "string",
-    required: true,
-  })
-  .option("price-ids", {
-    description:
-      "Space separated price feed ids (in hex) to fetch" +
-      " e.g: 0xf9c0172ba10dfa4d19088d...",
-    type: "array",
-    required: true,
-  })
-  .option("mnemonic", {
-    description: "Mnemonic (private key) for sender",
-    type: "string",
-    required: true,
-  })
-  .help()
-  .alias("help", "h")
-  .parserConfiguration({
-    "parse-numbers": false,
-  })
-  .parseSync();
-
-const network = argv.network;
-const pythContractAddr = argv.pythContract;
-
-const connection = new EvmPriceServiceConnection(argv.endpoint);
-
-async function run() {
-  const provider = new HDWalletProvider({
-    mnemonic: {
-      phrase: argv.mnemonic,
-    },
-    providerOrUrl: network,
-  });
-
-  // @ts-ignore
-  const web3 = new Web3(provider);
-  const priceIds = argv.priceIds as string[];
-
-  const priceFeeds = await connection.getLatestPriceFeeds(priceIds);
-  console.log(priceFeeds);
-
-  const priceFeedUpdateData =
-    await connection.getPriceFeedsUpdateData(priceIds);
-  console.log(priceFeedUpdateData);
-
-  const pythContract = new web3.eth.Contract(
-    PythInterfaceAbi as any,
-    pythContractAddr,
-    {
-      from: provider.getAddress(0),
-    },
-  );
-
-  const updateFee = await pythContract.methods
-    .getUpdateFee(priceFeedUpdateData)
-    .call();
-  console.log(`Update fee: ${updateFee}`);
-
-  let txHash = undefined;
-  await pythContract.methods
-    .updatePriceFeeds(priceFeedUpdateData)
-    .send({ value: updateFee })
-    .on("transactionHash", (hash: string) => {
-      txHash = hash;
-    })
-    .on("error", (err: any, receipt: any) => {
-      console.error(receipt);
-      throw err;
-    });
-
-  console.log(`Tx hash: ${txHash}`);
-  if (txHash === undefined) {
-    console.error("Something went wrong. Could not send price update tx.");
-  } else {
-    console.log("Awaiting tx confirmation...");
-    let receipt = undefined;
-    while (!receipt) {
-      receipt = await web3.eth.getTransactionReceipt(txHash);
-    }
-
-    // For on-chain use, you will typically perform the getPriceNoOlderThan call within the same transaction as updatePriceFeeds.
-    // The call to getPriceNoOlderThan below simply demonstrates that the on-chain price was in fact updated.
-    // Note that the code above for waiting for tx confirmation is a little flaky -- if so, you may see an old price printed here.
-    for (const priceId of priceIds) {
-      const [price, conf, expo, publishTime] = await pythContract.methods
-        .getPriceNoOlderThan(priceId, 60) // 60 seconds staleness tolerance
-        .call();
-      console.log(
-        `Updated ${priceId} to (${price} +- ${conf}) * 10^${expo} at unix timestamp ${publishTime}`,
-      );
-    }
-  }
-
-  provider.engine.stop();
-}
-
-run();

+ 242 - 0
target_chains/ethereum/sdk/js/src/filler.ts

@@ -0,0 +1,242 @@
+import { HermesClient } from "@pythnetwork/hermes-client";
+import {
+  Address,
+  PublicClient,
+  encodeFunctionData,
+  Hex,
+  Transport,
+  Chain,
+} from "viem";
+
+import { IPythAbi } from "./pyth-abi";
+import {
+  debugTraceCallAction,
+  extractPythPriceFeedsFromDebugTraceCall,
+} from "./tracer/debug-trace-call";
+import {
+  extractPythPriceFeedsFromTraceCallMany,
+  traceCallManyAction,
+} from "./tracer/trace-call-many";
+
+/**
+ * Represents a call request to be executed on the blockchain
+ */
+export type CallRequest = {
+  /** The address making the call (optional) */
+  from?: Address;
+  /** The target contract address */
+  to: Address;
+  /** The encoded function call data (optional) */
+  data?: Hex;
+  /** The amount of ETH to send with the call (optional) */
+  value?: bigint;
+};
+
+/**
+ * Get the update fee for a given set of update data
+ *
+ * @param client - The public client instance
+ * @param pythContractAddress - The Pyth contract address
+ * @param updateData - Array of hex-encoded update data
+ * @returns Promise resolving to the update fee in wei
+ */
+export async function getUpdateFee<
+  transport extends Transport,
+  chain extends Chain | undefined,
+>(
+  client: PublicClient<transport, chain>,
+  pythContractAddress: Address,
+  updateData: Hex[],
+): Promise<bigint> {
+  return await client.readContract({
+    address: pythContractAddress,
+    abi: IPythAbi,
+    functionName: "getUpdateFee",
+    args: [updateData],
+  });
+}
+
+/**
+ * A function that takes a Pyth update and a call request and returns a single bundled call request.
+ * This is used to combine the Pyth update with the original call.
+ */
+export type Bundler = (
+  pythUpdate: PythUpdate,
+  call: CallRequest,
+) => CallRequest;
+
+/**
+ * Tracing configuration options
+ */
+export type Config = {
+  /** Maximum number of iterations to find all required price feeds. Default is 5. */
+  maxIter?: number;
+} & (
+  | {
+      /**
+       * Use this when you want to trace multiple separate transactions (Pyth update + original call).
+       * This method traces each call independently, which may be more accurate but requires more RPC calls.
+       */
+      method: "trace_callMany";
+    }
+  | {
+      /**
+       * Use this when you want to trace a single bundled transaction that combines the Pyth update with the original call.
+       * The bundler function is responsible for creating a single transaction that executes both operations.
+       *
+       * The bundler is crucial because debug_traceCall can only trace one transaction at a time.
+       * The bundler must create a single call that includes both the Pyth price update and the original transaction logic.
+       * This allows the tracer to see all the Pyth price feed calls that would be made in the actual execution.
+       */
+      method: "debug_traceCall";
+      /**
+       * Function that takes a Pyth update and original call, returns a single bundled call request.
+       * Common bundlers include multicall3Bundler for combining calls via Multicall3 contract.
+       */
+      bundler: Bundler;
+    }
+);
+
+/**
+ * Represents a Pyth price update transaction
+ */
+export type PythUpdate = {
+  /** The call request to update Pyth price feeds */
+  call: CallRequest;
+  /** Array of hex-encoded price update data */
+  updateData: Hex[];
+  /** The fee required for the update in wei */
+  updateFee: bigint;
+};
+
+/**
+ * Fill the Pyth data for a given call request.
+ * Requires a client that supports trace_callMany or debug_traceCall with a bundler.
+ * This function will trace the call and find all the Pyth price feeds that are needed to fill the call in multiple
+ * iterations because a single call might revert if it requires a price feed that is not available and we need to
+ * trace the call again with the new price feeds until we have all the price feeds.
+ *
+ * @param client - The public client instance
+ * @param call - The call request to fill with Pyth data
+ * @param pythContractAddress - The Pyth contract address
+ * @param hermesEndpoint - The Hermes endpoint URL for fetching price updates
+ * @param config - Configuration options for tracing and bundling. Default is `{ method: "trace_callMany" }`.
+ *   - `Config` with `method: "trace_callMany"`: For trace_callMany method which traces multiple calls separately.
+ *     This method traces the Pyth update and original call as separate transactions.
+ *   - `Config` with `method: "debug_traceCall"` and `bundler`: For debug_traceCall method with a bundler function to
+ *     combine Pyth update with the original call. The bundler creates a single transaction that executes both the
+ *     Pyth update and the original call.
+ *   - `maxIter`: Maximum number of iterations to find all required price feeds. Each iteration traces the current
+ *     transaction(s) to find new Pyth price feed calls. The process stops when no new price feeds are found
+ *     or when maxIter is reached. Default is 5.
+ * @returns Promise resolving to Pyth update object or undefined if no Pyth data needed
+ */
+export async function fillPythUpdate<
+  transport extends Transport,
+  chain extends Chain | undefined,
+>(
+  client: PublicClient<transport, chain>,
+  call: CallRequest,
+  pythContractAddress: Address,
+  hermesEndpoint: string,
+  config?: Config,
+): Promise<PythUpdate | undefined> {
+  config = {
+    method: "trace_callMany",
+    ...config,
+  };
+
+  const hermesClient = new HermesClient(hermesEndpoint);
+
+  let requiredPriceFeeds = new Set<Address>();
+  let pythUpdate: PythUpdate | undefined;
+
+  for (let i = 0; i < (config.maxIter ?? 5); i++) {
+    const priceFeeds = await getPriceFeeds(
+      client,
+      pythContractAddress,
+      call,
+      config,
+      pythUpdate,
+    );
+
+    if (priceFeeds.isSubsetOf(requiredPriceFeeds)) {
+      break;
+    } else {
+      requiredPriceFeeds = requiredPriceFeeds.union(priceFeeds);
+      pythUpdate = await getPythUpdate(
+        client,
+        hermesClient,
+        requiredPriceFeeds,
+        pythContractAddress,
+        call,
+      );
+    }
+  }
+
+  return pythUpdate;
+}
+
+const getPythUpdate = async <
+  transport extends Transport,
+  chain extends Chain | undefined,
+>(
+  client: PublicClient<transport, chain>,
+  hermesClient: HermesClient,
+  priceFeeds: Set<Address>,
+  pythContractAddress: Address,
+  call: CallRequest,
+) => {
+  const hermesResponse = await hermesClient.getLatestPriceUpdates([
+    ...priceFeeds,
+  ]);
+  const updateData = hermesResponse.binary.data.map<Hex>((data) => `0x${data}`);
+  const updateFee = await getUpdateFee(client, pythContractAddress, updateData);
+  return {
+    call: {
+      to: pythContractAddress,
+      data: encodeFunctionData({
+        abi: IPythAbi,
+        functionName: "updatePriceFeeds",
+        args: [updateData],
+      }),
+      from: call.from,
+      value: updateFee,
+    },
+    updateData,
+    updateFee,
+  };
+};
+
+/**
+ * Get the price feeds from the trace of the given call.
+ */
+const getPriceFeeds = async <
+  transport extends Transport,
+  chain extends Chain | undefined,
+>(
+  client: PublicClient<transport, chain>,
+  pythContractAddress: Address,
+  call: CallRequest,
+  config: Config,
+  pythUpdate: PythUpdate | undefined,
+) => {
+  switch (config.method) {
+    case "debug_traceCall": {
+      return extractPythPriceFeedsFromDebugTraceCall(
+        await client
+          .extend(debugTraceCallAction)
+          .debugTraceCall(pythUpdate ? config.bundler(pythUpdate, call) : call),
+        pythContractAddress,
+      );
+    }
+    case "trace_callMany": {
+      return extractPythPriceFeedsFromTraceCallMany(
+        await client
+          .extend(traceCallManyAction)
+          .traceCallMany(pythUpdate ? [pythUpdate.call, call] : [call]),
+        pythContractAddress,
+      );
+    }
+  }
+};

+ 3 - 175
target_chains/ethereum/sdk/js/src/index.ts

@@ -1,175 +1,3 @@
-export { EvmPriceServiceConnection } from "./EvmPriceServiceConnection";
-
-export {
-  DurationInMs,
-  HexString,
-  Price,
-  PriceFeed,
-  PriceServiceConnectionConfig,
-  UnixTimestamp,
-} from "@pythnetwork/price-service-client";
-
-export const CONTRACT_ADDR: Record<string, string> = {
-  // Mainnets
-  apechain_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  arbitrum: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
-  astar_zkevm: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  aurora: "0xF89C7b475821EC3fDC2dC8099032c05c6c0c9AB9",
-  avalanche: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
-  blast: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  bnb: "0x4D7E825f80bDf85e913E0DD2A2D54927e9dE1594",
-  base: "0x8250f4aF4B972684F7b336503E2D6dFeDeB1487a",
-  boba: "0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF",
-  bttc: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  canto: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-  celo: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
-  chiliz: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  conflux_espace: "0xe9d69CdD6Fe41e7B621B4A688C5D1a68cB5c8ADc",
-  core_dao: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  cronos: "0xE0d0e68297772Dd5a1f1D99897c581E2082dbA5B",
-  cronos_zkevm_mainnet: "0x056f829183ec806a78c26c98961678c24fab71af",
-  eos: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  evmos: "0x354bF866A4B006C9AF9d9e06d9364217A8616E12",
-  ethereum: "0x4305FB66699C3B2702D4d05CF36551390A4c69C6",
-  etherlink: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  eventum_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  fantom: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
-  fantom_sonic_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  filecoin: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  flow_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  gnosis: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  gravity: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  hedera: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  hemi_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  horizen_eon: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  idex_xchain_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  injective_inevm: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  iota: "0x8D254a21b3C86D32F7179855531CE99164721933",
-  kava: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  kcc: "0xE0d0e68297772Dd5a1f1D99897c581E2082dbA5B",
-  kaia: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  lightlink: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  linea: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  manta: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  mantle: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  merlin: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  meter: "0xbFe3f445653f2136b2FD1e6DdDb5676392E3AF16",
-  mode: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  morph: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  neon: "0x7f2dB085eFC3560AFF33865dD727225d91B4f9A5",
-  opbnb: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  optimism: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
-  parallel: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  polygon: "0xff1a0f4744e8582DF1aE09D5611b887B6a12925C",
-  polygon_zkevm: "0xC5E56d6b40F3e3B5fbfa266bCd35C37426537c65",
-  polynomial: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  ronin: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  scroll: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  sei_evm_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  shimmer: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  skate: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  superseed_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  viction: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  wemix: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  zetachain: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  zkfair: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  zksync_era: "0xf087c864AEccFb6A2Bf1Af6A0382B0d0f6c5D834",
-  // Testnets (Stable sources)
-  abstract_testnet: "0x47F2A9BDAd52d65b66287253cf5ca0D2b763b486",
-  apechain_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  arbitrum_blueberry: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  arbitrum_sepolia: "0x4374e5a8b9C22271E9EB878A2AA31DE97DF15DAF",
-  astar_zkevm_testnet: "0x8D254a21b3C86D32F7179855531CE99164721933",
-  astar_zkyoto_testnet: "0x8D254a21b3C86D32F7179855531CE99164721933",
-  aurora_testnet: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E",
-  bnb_testnet: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  base_sepolia: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  berachain_testnet_v2: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  blackbird: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  blast_s2_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  boba_goerli: "0x8D254a21b3C86D32F7179855531CE99164721933",
-  boba_sepolia: "0x8D254a21b3C86D32F7179855531CE99164721933",
-  bttc_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  cant_testnet: "0x26DD80569a8B23768A1d80869Ed7339e07595E85",
-  celo_alfajores: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E",
-  chiado: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-  chiliz_testnet: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
-  conflux_espace_testnet: "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21",
-  core_dao_testnet: "0x8D254a21b3C86D32F7179855531CE99164721933",
-  coredao_testnet_v2: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  cronos_testnet: "0x36825bf3Fbdf5a29E2d5148bfe7Dcf7B5639e320",
-  cronos_zkevm_testnet: "0xB1DB1498902F08E16E11F1a423ec9CCB9537E1D6",
-  dela_deperp_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  dela_mithreum_deperp_testnet: "0xe9d69CdD6Fe41e7B621B4A688C5D1a68cB5c8ADc",
-  ethena_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  etherlink_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  eos_testnet: "0x0708325268dF9F66270F1401206434524814508b",
-  eventum_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  evmos_testnet: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E",
-  sonic_blaze_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  fantom_sonic_testnet: "0x96124d1F6E44FfDf1fb5D6d74BB2DE1B7Fbe7376",
-  fantom_testnet: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  filecoin_calibration: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  flow_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  fuji: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
-  hedera_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  idex_xchain_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  injective_inevm_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  kakarot_sepolia: "0xe9d69CdD6Fe41e7B621B4A688C5D1a68cB5c8ADc",
-  kava_testnet: "0xfA25E653b44586dBbe27eE9d252192F0e4956683",
-  kcc_testnet: "0x74f09cb3c7e2A01865f424FD14F6dc9A14E3e94E",
-  kinto: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  kaia_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  kraken_ink_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  lightlink_pegasus_testnet: "0x5D289Ad1CE59fCC25b6892e7A303dfFf3a9f7167",
-  linea_goerli: "0xdF21D137Aadc95588205586636710ca2890538d5",
-  linea_sepolia: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  manta_testnet: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c",
-  manta_sepolia: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  mantle_sepolia: "0x98046Bd286715D3B0BC227Dd7a956b83D8978603",
-  merlin_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  meter_testnet: "0x5a71C07a0588074443545eE0c08fb0375564c3E4",
-  mode_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  monad_devnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  monad_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  morph_holesky_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  morph_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  movement_evm_devnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  movement_evm_devnet_imola: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  mumbai: "0xFC6bd9F9f0c6481c6Af3A7Eb46b296A5B85ed379",
-  neon_devnet: "0x0708325268dF9F66270F1401206434524814508b",
-  nighthawk: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  olive_testnet: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c",
-  opbnb_testnet: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c",
-  optimism_celestia_raspberry: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  optimism_goerli: "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21",
-  optimism_sepolia: "0x0708325268dF9F66270F1401206434524814508b",
-  orange_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  parallel_testnet: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
-  polygon_amoy: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  polygon_blackberry: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  polygon_zkevm_testnet: "0xFf255f800044225f54Af4510332Aa3D67CC77635",
-  polynomial_testnet: "0x23f0e8FAeE7bbb405E7A7C3d60138FCfd43d7509",
-  reya_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  saigon: "0xEbe57e8045F2F230872523bbff7374986E45C486",
-  sei_evm_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  scroll_sepolia: "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c",
-  sepolia: "0xDd24F84d36BF92C65F92307595335bdFab5Bbd21",
-  shimmer_testnet: "0x8D254a21b3C86D32F7179855531CE99164721933",
-  skate_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  soneium_minato_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  story_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  superseed_testnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  tabi_testnet: "0x5744Cbf430D99456a0A8771208b674F27f8EF0Fb",
-  taiko_hekla: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  taiko_mainnet: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  unichain_sepolia: "0x2880aB155794e7179c9eE2e38200202908C17B43",
-  viction_testnet: "0x5D289Ad1CE59fCC25b6892e7A303dfFf3a9f7167",
-  wemix_testnet: "0x26DD80569a8B23768A1d80869Ed7339e07595E85",
-  zetachain_testnet: "0x0708325268dF9F66270F1401206434524814508b",
-  zkfair_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-  zksync_era_goerli: "0x8739d5024B5143278E2b15Bd9e7C26f6CEc658F1",
-  zksync_era_sepolia: "0x056f829183Ec806A78c26C98961678c24faB71af",
-  // Testnets (Beta sources)
-  horizen_gobi_testnet: "0xA2aa501b19aff244D90cc15a4Cf739D2725B5729",
-};
+export * from "./filler";
+export * from "./multicall3-bundler";
+export * from "./pyth-abi";

+ 79 - 0
target_chains/ethereum/sdk/js/src/multicall3-bundler.ts

@@ -0,0 +1,79 @@
+import { encodeFunctionData } from "viem";
+
+import { CallRequest, PythUpdate } from "./filler";
+
+// Multicall3 contract address (deployed on most chains)
+export const MULTICALL3_ADDRESS = "0xcA11bde05977b3631167028862bE2a173976CA11";
+
+// Multicall3 ABI for the aggregate3 and aggregate3Value functions
+export const MULTICALL3_ABI = [
+  {
+    inputs: [
+      {
+        components: [
+          { name: "target", type: "address" },
+          { name: "allowFailure", type: "bool" },
+          { name: "value", type: "uint256" },
+          { name: "callData", type: "bytes" },
+        ],
+        name: "calls",
+        type: "tuple[]",
+      },
+    ],
+    name: "aggregate3Value",
+    outputs: [
+      {
+        components: [
+          { name: "success", type: "bool" },
+          { name: "returnData", type: "bytes" },
+        ],
+        name: "returnData",
+        type: "tuple[]",
+      },
+    ],
+    stateMutability: "payable",
+    type: "function",
+  },
+] as const;
+
+/**
+ * Bundle multiple calls using Multicall3. This bundler will change the msg.sender of the calls to the
+ * Multicall3 contract and this might affect the result of the calls.
+ */
+export function multicall3Bundler(
+  pythUpdate: PythUpdate,
+  call: CallRequest,
+): CallRequest {
+  // Encode the multicall3 aggregate3 function call
+  const multicallData = encodeFunctionData({
+    abi: MULTICALL3_ABI,
+    functionName: "aggregate3Value",
+    args: [
+      [
+        {
+          target: pythUpdate.call.to,
+          allowFailure: false,
+          value: pythUpdate.call.value ?? 0n,
+          callData: pythUpdate.call.data ?? "0x",
+        },
+        {
+          target: call.to,
+          allowFailure: false,
+          value: call.value ?? 0n,
+          callData: call.data ?? "0x",
+        },
+      ],
+    ],
+  });
+
+  // Calculate total value needed
+  const totalValue = (call.value ?? 0n) + (pythUpdate.call.value ?? 0n);
+
+  // Create the bundled transaction that calls multicall3
+  return {
+    to: MULTICALL3_ADDRESS,
+    data: multicallData,
+    value: totalValue,
+    from: call.from,
+  };
+}

+ 660 - 0
target_chains/ethereum/sdk/js/src/pyth-abi.ts

@@ -0,0 +1,660 @@
+export const IPythAbi = [
+  {
+    anonymous: false,
+    inputs: [
+      {
+        indexed: true,
+        internalType: "bytes32",
+        name: "id",
+        type: "bytes32",
+      },
+      {
+        indexed: false,
+        internalType: "uint64",
+        name: "publishTime",
+        type: "uint64",
+      },
+      {
+        indexed: false,
+        internalType: "int64",
+        name: "price",
+        type: "int64",
+      },
+      {
+        indexed: false,
+        internalType: "uint64",
+        name: "conf",
+        type: "uint64",
+      },
+    ],
+    name: "PriceFeedUpdate",
+    type: "event",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32",
+        name: "id",
+        type: "bytes32",
+      },
+    ],
+    name: "getEmaPrice",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "int64",
+            name: "price",
+            type: "int64",
+          },
+          {
+            internalType: "uint64",
+            name: "conf",
+            type: "uint64",
+          },
+          {
+            internalType: "int32",
+            name: "expo",
+            type: "int32",
+          },
+          {
+            internalType: "uint256",
+            name: "publishTime",
+            type: "uint256",
+          },
+        ],
+        internalType: "struct PythStructs.Price",
+        name: "price",
+        type: "tuple",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32",
+        name: "id",
+        type: "bytes32",
+      },
+      {
+        internalType: "uint256",
+        name: "age",
+        type: "uint256",
+      },
+    ],
+    name: "getEmaPriceNoOlderThan",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "int64",
+            name: "price",
+            type: "int64",
+          },
+          {
+            internalType: "uint64",
+            name: "conf",
+            type: "uint64",
+          },
+          {
+            internalType: "int32",
+            name: "expo",
+            type: "int32",
+          },
+          {
+            internalType: "uint256",
+            name: "publishTime",
+            type: "uint256",
+          },
+        ],
+        internalType: "struct PythStructs.Price",
+        name: "price",
+        type: "tuple",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32",
+        name: "id",
+        type: "bytes32",
+      },
+    ],
+    name: "getEmaPriceUnsafe",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "int64",
+            name: "price",
+            type: "int64",
+          },
+          {
+            internalType: "uint64",
+            name: "conf",
+            type: "uint64",
+          },
+          {
+            internalType: "int32",
+            name: "expo",
+            type: "int32",
+          },
+          {
+            internalType: "uint256",
+            name: "publishTime",
+            type: "uint256",
+          },
+        ],
+        internalType: "struct PythStructs.Price",
+        name: "price",
+        type: "tuple",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32",
+        name: "id",
+        type: "bytes32",
+      },
+    ],
+    name: "getPrice",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "int64",
+            name: "price",
+            type: "int64",
+          },
+          {
+            internalType: "uint64",
+            name: "conf",
+            type: "uint64",
+          },
+          {
+            internalType: "int32",
+            name: "expo",
+            type: "int32",
+          },
+          {
+            internalType: "uint256",
+            name: "publishTime",
+            type: "uint256",
+          },
+        ],
+        internalType: "struct PythStructs.Price",
+        name: "price",
+        type: "tuple",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32",
+        name: "id",
+        type: "bytes32",
+      },
+      {
+        internalType: "uint256",
+        name: "age",
+        type: "uint256",
+      },
+    ],
+    name: "getPriceNoOlderThan",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "int64",
+            name: "price",
+            type: "int64",
+          },
+          {
+            internalType: "uint64",
+            name: "conf",
+            type: "uint64",
+          },
+          {
+            internalType: "int32",
+            name: "expo",
+            type: "int32",
+          },
+          {
+            internalType: "uint256",
+            name: "publishTime",
+            type: "uint256",
+          },
+        ],
+        internalType: "struct PythStructs.Price",
+        name: "price",
+        type: "tuple",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes32",
+        name: "id",
+        type: "bytes32",
+      },
+    ],
+    name: "getPriceUnsafe",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "int64",
+            name: "price",
+            type: "int64",
+          },
+          {
+            internalType: "uint64",
+            name: "conf",
+            type: "uint64",
+          },
+          {
+            internalType: "int32",
+            name: "expo",
+            type: "int32",
+          },
+          {
+            internalType: "uint256",
+            name: "publishTime",
+            type: "uint256",
+          },
+        ],
+        internalType: "struct PythStructs.Price",
+        name: "price",
+        type: "tuple",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes[]",
+        name: "updateData",
+        type: "bytes[]",
+      },
+    ],
+    name: "getUpdateFee",
+    outputs: [
+      {
+        internalType: "uint256",
+        name: "feeAmount",
+        type: "uint256",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [],
+    name: "getValidTimePeriod",
+    outputs: [
+      {
+        internalType: "uint256",
+        name: "validTimePeriod",
+        type: "uint256",
+      },
+    ],
+    stateMutability: "view",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes[]",
+        name: "updateData",
+        type: "bytes[]",
+      },
+      {
+        internalType: "bytes32[]",
+        name: "priceIds",
+        type: "bytes32[]",
+      },
+      {
+        internalType: "uint64",
+        name: "minPublishTime",
+        type: "uint64",
+      },
+      {
+        internalType: "uint64",
+        name: "maxPublishTime",
+        type: "uint64",
+      },
+    ],
+    name: "parsePriceFeedUpdates",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "bytes32",
+            name: "id",
+            type: "bytes32",
+          },
+          {
+            components: [
+              {
+                internalType: "int64",
+                name: "price",
+                type: "int64",
+              },
+              {
+                internalType: "uint64",
+                name: "conf",
+                type: "uint64",
+              },
+              {
+                internalType: "int32",
+                name: "expo",
+                type: "int32",
+              },
+              {
+                internalType: "uint256",
+                name: "publishTime",
+                type: "uint256",
+              },
+            ],
+            internalType: "struct PythStructs.Price",
+            name: "price",
+            type: "tuple",
+          },
+          {
+            components: [
+              {
+                internalType: "int64",
+                name: "price",
+                type: "int64",
+              },
+              {
+                internalType: "uint64",
+                name: "conf",
+                type: "uint64",
+              },
+              {
+                internalType: "int32",
+                name: "expo",
+                type: "int32",
+              },
+              {
+                internalType: "uint256",
+                name: "publishTime",
+                type: "uint256",
+              },
+            ],
+            internalType: "struct PythStructs.Price",
+            name: "emaPrice",
+            type: "tuple",
+          },
+        ],
+        internalType: "struct PythStructs.PriceFeed[]",
+        name: "priceFeeds",
+        type: "tuple[]",
+      },
+    ],
+    stateMutability: "payable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes[]",
+        name: "updateData",
+        type: "bytes[]",
+      },
+      {
+        internalType: "bytes32[]",
+        name: "priceIds",
+        type: "bytes32[]",
+      },
+      {
+        internalType: "uint64",
+        name: "minPublishTime",
+        type: "uint64",
+      },
+      {
+        internalType: "uint64",
+        name: "maxPublishTime",
+        type: "uint64",
+      },
+    ],
+    name: "parsePriceFeedUpdatesUnique",
+    outputs: [
+      {
+        components: [
+          {
+            internalType: "bytes32",
+            name: "id",
+            type: "bytes32",
+          },
+          {
+            components: [
+              {
+                internalType: "int64",
+                name: "price",
+                type: "int64",
+              },
+              {
+                internalType: "uint64",
+                name: "conf",
+                type: "uint64",
+              },
+              {
+                internalType: "int32",
+                name: "expo",
+                type: "int32",
+              },
+              {
+                internalType: "uint256",
+                name: "publishTime",
+                type: "uint256",
+              },
+            ],
+            internalType: "struct PythStructs.Price",
+            name: "price",
+            type: "tuple",
+          },
+          {
+            components: [
+              {
+                internalType: "int64",
+                name: "price",
+                type: "int64",
+              },
+              {
+                internalType: "uint64",
+                name: "conf",
+                type: "uint64",
+              },
+              {
+                internalType: "int32",
+                name: "expo",
+                type: "int32",
+              },
+              {
+                internalType: "uint256",
+                name: "publishTime",
+                type: "uint256",
+              },
+            ],
+            internalType: "struct PythStructs.Price",
+            name: "emaPrice",
+            type: "tuple",
+          },
+        ],
+        internalType: "struct PythStructs.PriceFeed[]",
+        name: "priceFeeds",
+        type: "tuple[]",
+      },
+    ],
+    stateMutability: "payable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes[]",
+        name: "updateData",
+        type: "bytes[]",
+      },
+    ],
+    name: "updatePriceFeeds",
+    outputs: [],
+    stateMutability: "payable",
+    type: "function",
+  },
+  {
+    inputs: [
+      {
+        internalType: "bytes[]",
+        name: "updateData",
+        type: "bytes[]",
+      },
+      {
+        internalType: "bytes32[]",
+        name: "priceIds",
+        type: "bytes32[]",
+      },
+      {
+        internalType: "uint64[]",
+        name: "publishTimes",
+        type: "uint64[]",
+      },
+    ],
+    name: "updatePriceFeedsIfNecessary",
+    outputs: [],
+    stateMutability: "payable",
+    type: "function",
+  },
+] as const;
+
+export const IPythEventsAbi = [
+  {
+    anonymous: false,
+    inputs: [
+      {
+        indexed: true,
+        internalType: "bytes32",
+        name: "id",
+        type: "bytes32",
+      },
+      {
+        indexed: false,
+        internalType: "uint64",
+        name: "publishTime",
+        type: "uint64",
+      },
+      {
+        indexed: false,
+        internalType: "int64",
+        name: "price",
+        type: "int64",
+      },
+      {
+        indexed: false,
+        internalType: "uint64",
+        name: "conf",
+        type: "uint64",
+      },
+    ],
+    name: "PriceFeedUpdate",
+    type: "event",
+  },
+] as const;
+
+export const PythErrorsAbi = [
+  {
+    inputs: [],
+    name: "InsufficientFee",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "InvalidArgument",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "InvalidGovernanceDataSource",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "InvalidGovernanceMessage",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "InvalidGovernanceTarget",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "InvalidUpdateData",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "InvalidUpdateDataSource",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "InvalidWormholeAddressToSet",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "InvalidWormholeVaa",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "NoFreshUpdate",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "OldGovernanceMessage",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "PriceFeedNotFound",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "PriceFeedNotFoundWithinRange",
+    type: "error",
+  },
+  {
+    inputs: [],
+    name: "StalePrice",
+    type: "error",
+  },
+] as const;
+
+export const PythAbi = [
+  ...IPythAbi,
+  ...IPythEventsAbi,
+  ...PythErrorsAbi,
+] as const;

+ 129 - 0
target_chains/ethereum/sdk/js/src/tracer/debug-trace-call.ts

@@ -0,0 +1,129 @@
+import {
+  Address,
+  BlockTag,
+  CallParameters,
+  Client,
+  decodeFunctionData,
+  ExactPartial,
+  formatTransactionRequest,
+  Hex,
+  isAddressEqual,
+  RpcTransactionRequest,
+} from "viem";
+
+import { IPythAbi } from "../pyth-abi";
+
+/**
+ * Extract Pyth price feed IDs from a transaction call trace.
+ */
+export function extractPythPriceFeedsFromDebugTraceCall(
+  trace: RpcCallTrace,
+  pythContractAddress: Address,
+  ignoreParsingErrors = false,
+): Set<Hex> {
+  const result = new Set<Hex>();
+  if (isAddressEqual(trace.to, pythContractAddress)) {
+    // Decode the calldata to see what function is being called
+    try {
+      const decoded = decodeFunctionData({
+        abi: IPythAbi,
+        data: trace.input,
+      });
+
+      let priceFeedId: Hex | undefined;
+      switch (decoded.functionName) {
+        case "getPrice":
+        case "getPriceNoOlderThan":
+        case "getPriceUnsafe":
+        case "getEmaPrice":
+        case "getEmaPriceNoOlderThan":
+        case "getEmaPriceUnsafe": {
+          priceFeedId = decoded.args[0];
+          break;
+        }
+        default: {
+          break;
+        }
+      }
+      if (priceFeedId !== undefined) {
+        result.add(priceFeedId);
+      }
+    } catch (error: unknown) {
+      if (!ignoreParsingErrors) {
+        const thrownError = new Error(
+          `Failed to decode calldata: ${trace.input}. Make sure correct Pyth contract address is used.`,
+        );
+        thrownError.cause = error;
+        throw thrownError;
+      }
+    }
+  }
+  if (trace.calls === undefined) {
+    return result;
+  }
+  return new Set([
+    ...result,
+    ...trace.calls.flatMap((call) => [
+      ...extractPythPriceFeedsFromDebugTraceCall(call, pythContractAddress),
+    ]),
+  ]);
+}
+
+export type TraceCallRpcSchema = {
+  Method: "debug_traceCall";
+  Parameters:
+    | [ExactPartial<RpcTransactionRequest>, Hex | BlockTag]
+    | [
+        ExactPartial<RpcTransactionRequest>,
+        BlockTag | Hex,
+        {
+          tracer: "callTracer" | "prestateTracer";
+          tracerConfig?: { onlyTopCall?: boolean; withLog?: boolean };
+        },
+      ];
+  ReturnType: RpcCallTrace;
+};
+
+export type RpcCallType =
+  | "CALL"
+  | "STATICCALL"
+  | "DELEGATECALL"
+  | "CREATE"
+  | "CREATE2"
+  | "SELFDESTRUCT"
+  | "CALLCODE";
+
+export type RpcLogTrace = {
+  address: Address;
+  data: Hex;
+  position: Hex;
+  topics: [Hex, ...Hex[]];
+};
+
+export type RpcCallTrace = {
+  from: Address;
+  gas: Hex;
+  gasUsed: Hex;
+  to: Address;
+  input: Hex;
+  output: Hex;
+  error?: string;
+  revertReason?: string;
+  calls?: RpcCallTrace[];
+  logs?: RpcLogTrace[];
+  value?: Hex;
+  type: RpcCallType;
+};
+
+export const debugTraceCallAction = (client: Client) => ({
+  async debugTraceCall(args: CallParameters) {
+    return client.request<TraceCallRpcSchema>({
+      method: "debug_traceCall",
+      params: [
+        formatTransactionRequest(args),
+        "latest",
+        { tracer: "callTracer" },
+      ],
+    });
+  },
+});

+ 118 - 0
target_chains/ethereum/sdk/js/src/tracer/trace-call-many.ts

@@ -0,0 +1,118 @@
+import {
+  Address,
+  BlockTag,
+  decodeFunctionData,
+  RpcTransactionRequest,
+  ExactPartial,
+  Hex,
+  Client,
+  formatTransactionRequest,
+  CallParameters,
+  isAddressEqual,
+} from "viem";
+
+import { IPythAbi } from "../pyth-abi";
+
+/**
+ * Extract Pyth price feed IDs from a transaction call trace.
+ */
+export function extractPythPriceFeedsFromTraceCallMany(
+  traceResults: TraceCallResult[],
+  pythContractAddress: Address,
+  ignoreParsingErrors = false,
+): Set<`0x${string}`> {
+  const result = new Set<`0x${string}`>();
+  for (const traceResult of traceResults) {
+    for (const trace of traceResult.trace) {
+      if (isAddressEqual(trace.action.to, pythContractAddress)) {
+        // Decode the calldata to see what function is being called
+        try {
+          const decoded = decodeFunctionData({
+            abi: IPythAbi,
+            data: trace.action.input,
+          });
+
+          let priceFeedId: `0x${string}` | undefined;
+          switch (decoded.functionName) {
+            case "getPrice":
+            case "getPriceNoOlderThan":
+            case "getPriceUnsafe":
+            case "getEmaPrice":
+            case "getEmaPriceNoOlderThan":
+            case "getEmaPriceUnsafe": {
+              priceFeedId = decoded.args[0];
+              break;
+            }
+            default: {
+              break;
+            }
+          }
+          if (priceFeedId !== undefined) {
+            result.add(priceFeedId);
+          }
+        } catch {
+          if (!ignoreParsingErrors) {
+            throw new Error(
+              `Failed to decode calldata: ${trace.action.input}. Make sure correct Pyth contract address is used.`,
+            );
+          }
+        }
+      }
+    }
+  }
+
+  return result;
+}
+
+export type TraceCallResult = {
+  output: Hex;
+  stateDiff: undefined;
+  trace: RpcCallTrace[];
+  vmTrace: undefined;
+};
+
+export type TraceCallRpcSchema = {
+  Method: "trace_callMany";
+  Parameters: [
+    [ExactPartial<RpcTransactionRequest>, ["trace"]][],
+    Hex | BlockTag,
+  ];
+  ReturnType: TraceCallResult[];
+};
+
+export type RpcCallTrace = {
+  action: {
+    from: Address;
+    callType:
+      | "call"
+      | "staticcall"
+      | "delegatecall"
+      | "create"
+      | "create2"
+      | "selfdestruct"
+      | "callcode";
+    gas: Hex;
+    input: Hex;
+    to: Address;
+    value: Hex;
+  };
+  result: {
+    gasUsed: Hex;
+    output: Hex;
+  };
+  subtraces: number;
+  traceAddress: number[];
+  type: "call" | "create";
+};
+
+export const traceCallManyAction = (client: Client) => ({
+  async traceCallMany(args: CallParameters[]) {
+    return client.request<TraceCallRpcSchema>({
+      method: "trace_callMany",
+      params: [
+        args.map((a) => [formatTransactionRequest(a), ["trace"]]),
+        "latest",
+      ],
+    });
+  },
+});

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff