Parcourir la source

refactor(apps/price_pusher): Use viem instead of web3 for evm pusher (#1829)

Web3 library is not very widely used anymore due to its complex and magical internal design. Some users were reporting issues like strange timeouts or unsupported RPC calls via web3 (and the deprecated HDWalletProvider by truffle that we use). This change refactors the code to use Viem. The experience with Viem is nice: it has strong types and good utilities. The error handling is also very well designed. The downside is that it has a steep learning curve to get it right.

This change refactors the code based on the PR reviews to make it simpler as well.

Lastly the version is bumped as a major release because the behaviour and logs have changed and it might affect production environments. It also signals the users to test it out properly before using it which is good because all the failure cases might not be handled.
Ali Behjati il y a 1 an
Parent
commit
5c3be6ad11

+ 2 - 5
apps/price_pusher/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/price-pusher",
-  "version": "7.1.0",
+  "version": "8.0.0-alpha",
   "description": "Pyth Price Pusher",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",
@@ -64,16 +64,13 @@
     "@pythnetwork/pyth-sui-js": "workspace:*",
     "@pythnetwork/solana-utils": "workspace:*",
     "@solana/web3.js": "^1.93.0",
-    "@truffle/hdwallet-provider": "^2.1.3",
     "@types/pino": "^7.0.5",
     "aptos": "^1.8.5",
     "jito-ts": "^3.0.1",
     "joi": "^17.6.0",
     "near-api-js": "^3.0.2",
     "pino": "^9.2.0",
-    "web3": "^1.8.1",
-    "web3-core": "^1.8.1",
-    "web3-eth-contract": "^1.8.1",
+    "viem": "^2.19.4",
     "yaml": "^2.1.1",
     "yargs": "^17.5.1"
   }

+ 18 - 12
apps/price_pusher/src/evm/command.ts

@@ -5,9 +5,12 @@ import * as options from "../options";
 import { readPriceConfigFile } from "../price-config";
 import { PythPriceListener } from "../pyth-price-listener";
 import { Controller } from "../controller";
-import { EvmPriceListener, EvmPricePusher, PythContractFactory } from "./evm";
+import { EvmPriceListener, EvmPricePusher } from "./evm";
 import { getCustomGasStation } from "./custom-gas-station";
 import pino from "pino";
+import { createClient } from "./super-wallet";
+import { createPythContract } from "./pyth-contract";
+import { isWsEndpoint } from "../utils";
 
 export default {
   command: "evm",
@@ -77,7 +80,7 @@ export default {
     ...options.priceServiceConnectionLogLevel,
     ...options.controllerLogLevel,
   },
-  handler: function (argv: any) {
+  handler: async function (argv: any) {
     // FIXME: type checks for this
     const {
       endpoint,
@@ -121,20 +124,22 @@ export default {
       logger.child({ module: "PythPriceListener" })
     );
 
-    const pythContractFactory = new PythContractFactory(
-      endpoint,
-      mnemonic,
-      pythContractAddress
-    );
+    const client = await createClient(endpoint, mnemonic);
+    const pythContract = createPythContract(client, pythContractAddress);
+
     logger.info(
-      `Pushing updates from wallet address: ${pythContractFactory
-        .createWeb3PayerProvider()
-        .getAddress()}`
+      `Pushing updates from wallet address: ${client.account.address}`
     );
 
+    // It is possible to watch the events in the non-ws endpoints, either by getFilter
+    // or by getLogs, but it is very expensive and our polling mechanism does it
+    // in a more efficient way. So we only do it with ws endpoints.
+    const watchEvents = isWsEndpoint(endpoint);
+
     const evmListener = new EvmPriceListener(
-      pythContractFactory,
+      pythContract,
       priceItems,
+      watchEvents,
       logger.child({ module: "EvmPriceListener" }),
       {
         pollingFrequency,
@@ -148,7 +153,8 @@ export default {
     );
     const evmPusher = new EvmPricePusher(
       priceServiceConnection,
-      pythContractFactory,
+      client,
+      pythContract,
       logger.child({ module: "EvmPricePusher" }),
       overrideGasPriceMultiplier,
       overrideGasPriceMultiplierCap,

+ 4 - 5
apps/price_pusher/src/evm/custom-gas-station.ts

@@ -1,4 +1,3 @@
-import Web3 from "web3";
 import {
   CustomGasChainId,
   TxSpeed,
@@ -7,8 +6,9 @@ import {
   customGasChainIds,
 } from "../utils";
 import { Logger } from "pino";
+import { parseGwei } from "viem";
 
-type chainMethods = Record<CustomGasChainId, () => Promise<string | undefined>>;
+type chainMethods = Record<CustomGasChainId, () => Promise<bigint | undefined>>;
 
 export class CustomGasStation {
   private chain: CustomGasChainId;
@@ -29,11 +29,10 @@ export class CustomGasStation {
 
   private async fetchMaticMainnetGasPrice() {
     try {
-      const res = await fetch("https://gasstation-mainnet.matic.network/v2");
+      const res = await fetch("https://gasstation.polygon.technology/v2");
       const jsonRes = await res.json();
       const gasPrice = jsonRes[this.speed].maxFee;
-      const gweiGasPrice = Web3.utils.toWei(gasPrice.toFixed(2), "Gwei");
-      return gweiGasPrice.toString();
+      return parseGwei(gasPrice.toFixed(2));
     } catch (err) {
       this.logger.error(
         err,

+ 215 - 220
apps/price_pusher/src/evm/evm.ts

@@ -1,18 +1,17 @@
-import { Contract, EventData } from "web3-eth-contract";
 import {
   IPricePusher,
   PriceInfo,
   ChainPriceListener,
   PriceItem,
 } from "../interface";
-import { TransactionReceipt } from "ethereum-protocol";
-import { addLeading0x, DurationInSeconds, removeLeading0x } from "../utils";
-import AbstractPythAbi from "@pythnetwork/pyth-sdk-solidity/abis/AbstractPyth.json";
-import HDWalletProvider from "@truffle/hdwallet-provider";
-import Web3 from "web3";
-import { HttpProvider, WebsocketProvider } from "web3-core";
+import {
+  addLeading0x,
+  assertDefined,
+  DurationInSeconds,
+  removeLeading0x,
+} from "../utils";
+import { PythAbi } from "./pyth-abi";
 import { Logger } from "pino";
-import { isWsEndpoint } from "../utils";
 import {
   PriceServiceConnection,
   HexString,
@@ -20,36 +19,41 @@ import {
 } from "@pythnetwork/price-service-client";
 import { CustomGasStation } from "./custom-gas-station";
 import { PushAttempt } from "../common";
-import { ProviderOrUrl } from "@truffle/hdwallet-provider/dist/constructor/types";
+import {
+  WatchContractEventOnLogsParameter,
+  TransactionExecutionError,
+  BaseError,
+  ContractFunctionRevertedError,
+  FeeCapTooLowError,
+  InternalRpcError,
+  InsufficientFundsError,
+} from "viem";
+
+import { PythContract } from "./pyth-contract";
+import { SuperWalletClient } from "./super-wallet";
 
 export class EvmPriceListener extends ChainPriceListener {
-  private pythContractFactory: PythContractFactory;
-  private pythContract: Contract;
-  private logger: Logger;
-
   constructor(
-    pythContractFactory: PythContractFactory,
+    private pythContract: PythContract,
     priceItems: PriceItem[],
-    logger: Logger,
+    private watchEvents: boolean,
+    private logger: Logger,
     config: {
       pollingFrequency: DurationInSeconds;
     }
   ) {
     super(config.pollingFrequency, priceItems);
 
-    this.pythContractFactory = pythContractFactory;
-    this.pythContract = this.pythContractFactory.createPythContract();
+    this.pythContract = pythContract;
     this.logger = logger;
   }
 
   // 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() {
-    if (this.pythContractFactory.hasWebsocketProvider()) {
-      this.logger.info(
-        "Subscribing to the target network pyth contract events..."
-      );
-      this.startSubscription();
+    if (this.watchEvents) {
+      this.logger.info("Watching target network pyth contract events...");
+      this.startWatching();
     } else {
       this.logger.info(
         "The target network RPC endpoint is not Websocket. " +
@@ -61,53 +65,44 @@ export class EvmPriceListener extends ChainPriceListener {
     await super.start();
   }
 
-  private async startSubscription() {
-    for (const { id: priceId } of this.priceItems) {
-      this.pythContract.events.PriceFeedUpdate(
-        {
-          filter: {
-            id: addLeading0x(priceId),
-            fresh: true,
-          },
-        },
-        this.onPriceFeedUpdate.bind(this)
-      );
-    }
+  private async startWatching() {
+    this.pythContract.watchEvent.PriceFeedUpdate(
+      { id: this.priceItems.map((item) => addLeading0x(item.id)) },
+      { strict: true, onLogs: this.onPriceFeedUpdate.bind(this) }
+    );
   }
 
-  private onPriceFeedUpdate(err: Error | null, event: EventData) {
-    if (err !== null) {
-      this.logger.error(
-        err,
-        "PriceFeedUpdate EventEmitter received an error.."
+  private onPriceFeedUpdate(
+    logs: WatchContractEventOnLogsParameter<typeof PythAbi, "PriceFeedUpdate">
+  ) {
+    for (const log of logs) {
+      const priceId = removeLeading0x(assertDefined(log.args.id));
+
+      const priceInfo: PriceInfo = {
+        conf: assertDefined(log.args.conf).toString(),
+        price: assertDefined(log.args.price).toString(),
+        publishTime: Number(assertDefined(log.args.publishTime)),
+      };
+
+      this.logger.debug(
+        { priceInfo },
+        `Received a new Evm PriceFeedUpdate event for price feed ${this.priceIdToAlias.get(
+          priceId
+        )} (${priceId}).`
       );
-      throw err;
-    }
 
-    const priceId = removeLeading0x(event.returnValues.id);
-    this.logger.debug(
-      `Received a new Evm PriceFeedUpdate event for price feed ${this.priceIdToAlias.get(
-        priceId
-      )} (${priceId}).`
-    );
-
-    const priceInfo: PriceInfo = {
-      conf: event.returnValues.conf,
-      price: event.returnValues.price,
-      publishTime: Number(event.returnValues.publishTime),
-    };
-
-    this.updateLatestPriceInfo(priceId, priceInfo);
+      this.updateLatestPriceInfo(priceId, priceInfo);
+    }
   }
 
   async getOnChainPriceInfo(
     priceId: HexString
   ): Promise<PriceInfo | undefined> {
-    let priceRaw;
+    let priceRaw: any;
     try {
-      priceRaw = await this.pythContract.methods
-        .getPriceUnsafe(addLeading0x(priceId))
-        .call();
+      priceRaw = await this.pythContract.read.getPriceUnsafe([
+        addLeading0x(priceId),
+      ]);
     } catch (err) {
       this.logger.error(err, `Polling on-chain price for ${priceId} failed.`);
       return undefined;
@@ -128,26 +123,20 @@ export class EvmPriceListener extends ChainPriceListener {
 }
 
 export class EvmPricePusher implements IPricePusher {
-  private customGasStation?: CustomGasStation;
-  private pythContract: Contract;
-  private web3: Web3;
-  private pusherAddress: string | undefined;
+  private pusherAddress: `0x${string}` | undefined;
   private lastPushAttempt: PushAttempt | undefined;
 
   constructor(
     private connection: PriceServiceConnection,
-    pythContractFactory: PythContractFactory,
+    private client: SuperWalletClient,
+    private pythContract: PythContract,
     private logger: Logger,
     private overrideGasPriceMultiplier: number,
     private overrideGasPriceMultiplierCap: number,
     private updateFeeMultiplier: number,
     private gasLimit?: number,
-    customGasStation?: CustomGasStation
-  ) {
-    this.customGasStation = customGasStation;
-    this.pythContract = pythContractFactory.createPythContractWithPayer();
-    this.web3 = new Web3(pythContractFactory.createWeb3PayerProvider() as any);
-  }
+    private customGasStation?: CustomGasStation
+  ) {}
 
   // The pubTimes are passed here to use the values that triggered the push.
   // This is an optimization to avoid getting a newer value (as an update comes)
@@ -168,17 +157,19 @@ export class EvmPricePusher implements IPricePusher {
 
     const priceIdsWith0x = priceIds.map((priceId) => addLeading0x(priceId));
 
-    const priceFeedUpdateData = await this.getPriceFeedsUpdateData(
+    const priceFeedUpdateData = (await this.getPriceFeedsUpdateData(
       priceIdsWith0x
-    );
+    )) as `0x${string}`[];
 
     let updateFee;
 
     try {
-      updateFee = await this.pythContract.methods
-        .getUpdateFee(priceFeedUpdateData)
-        .call();
-      updateFee = Number(updateFee) * (this.updateFeeMultiplier || 1);
+      updateFee = await this.pythContract.read.getUpdateFee([
+        priceFeedUpdateData,
+      ]);
+      updateFee = BigInt(
+        Math.round(Number(updateFee) * (this.updateFeeMultiplier || 1))
+      );
       this.logger.debug(`Update fee: ${updateFee}`);
     } catch (e: any) {
       this.logger.error(
@@ -188,17 +179,24 @@ export class EvmPricePusher implements IPricePusher {
       throw e;
     }
 
-    let gasPrice = Number(
-      (await this.customGasStation?.getCustomGasPrice()) ||
-        (await this.web3.eth.getGasPrice())
-    );
+    const fees = await this.client.estimateFeesPerGas();
+
+    this.logger.debug({ fees }, "Estimated fees");
+
+    let gasPrice =
+      Number(await this.customGasStation?.getCustomGasPrice()) ||
+      Number(fees.gasPrice) ||
+      Number(fees.maxFeePerGas);
 
     // Try to re-use the same nonce and increase the gas if the last tx is not landed yet.
     if (this.pusherAddress === undefined) {
-      this.pusherAddress = (await this.web3.eth.getAccounts())[0];
+      this.pusherAddress = this.client.account.address;
     }
+
     const lastExecutedNonce =
-      (await this.web3.eth.getTransactionCount(this.pusherAddress)) - 1;
+      (await this.client.getTransactionCount({
+        address: this.pusherAddress,
+      })) - 1;
 
     let gasPriceToOverride = undefined;
 
@@ -206,13 +204,15 @@ export class EvmPricePusher implements IPricePusher {
       if (this.lastPushAttempt.nonce <= lastExecutedNonce) {
         this.lastPushAttempt = undefined;
       } else {
-        gasPriceToOverride = Math.ceil(
-          this.lastPushAttempt.gasPrice * this.overrideGasPriceMultiplier
-        );
+        gasPriceToOverride =
+          this.lastPushAttempt.gasPrice * this.overrideGasPriceMultiplier;
       }
     }
 
-    if (gasPriceToOverride !== undefined && gasPriceToOverride > gasPrice) {
+    if (
+      gasPriceToOverride !== undefined &&
+      gasPriceToOverride > Number(gasPrice)
+    ) {
       gasPrice = Math.min(
         gasPriceToOverride,
         gasPrice * this.overrideGasPriceMultiplierCap
@@ -223,42 +223,108 @@ export class EvmPricePusher implements IPricePusher {
 
     this.logger.debug(`Using gas price: ${gasPrice} and nonce: ${txNonce}`);
 
-    this.pythContract.methods
-      .updatePriceFeedsIfNecessary(
-        priceFeedUpdateData,
-        priceIdsWith0x,
-        pubTimesToPush
-      )
-      .send({
-        value: updateFee,
-        gasPrice,
-        nonce: txNonce,
-        gasLimit: this.gasLimit,
-      })
-      .on("transactionHash", (hash: string) => {
-        this.logger.info({ hash }, "Price update successful");
-      })
-      .on("error", (err: Error, receipt?: TransactionReceipt) => {
-        if (err.message.includes("revert")) {
-          // Since we are using custom error structs on solidity the rejection
-          // doesn't return any information why the call has reverted. Assuming that
-          // the update data is valid there is no possible rejection cause other than
-          // the target chain price being already updated.
+    const pubTimesToPushParam = pubTimesToPush.map((pubTime) =>
+      BigInt(pubTime)
+    );
+
+    try {
+      const { request } =
+        await this.pythContract.simulate.updatePriceFeedsIfNecessary(
+          [priceFeedUpdateData, priceIdsWith0x, pubTimesToPushParam],
+          {
+            value: updateFee,
+            gasPrice: BigInt(Math.ceil(gasPrice)),
+            nonce: txNonce,
+            gas:
+              this.gasLimit !== undefined
+                ? BigInt(Math.ceil(this.gasLimit))
+                : undefined,
+          }
+        );
+
+      this.logger.debug({ request }, "Simulated request successfully");
+
+      const hash = await this.client.writeContract(request);
+
+      this.logger.info({ hash }, "Price update sent");
+
+      this.waitForTransactionReceipt(hash);
+    } catch (err: any) {
+      this.logger.debug({ err }, "Simulating or sending transactions failed.");
+
+      if (err instanceof BaseError) {
+        if (
+          err.walk(
+            (e) =>
+              e instanceof ContractFunctionRevertedError &&
+              e.data?.errorName === "NoFreshUpdate"
+          )
+        ) {
           this.logger.info(
-            { err, receipt },
-            "Execution reverted. With high probability, the target chain price " +
-              "has already updated, Skipping this push."
+            "Simulation reverted because none of the updates are fresh. This is an expected behaviour to save gas. Skipping this push."
+          );
+          return;
+        }
+
+        if (err.walk((e) => e instanceof InsufficientFundsError)) {
+          this.logger.error(
+            { err },
+            "Wallet doesn't have enough balance. In rare cases, there might be issues with gas price " +
+              "calculation in the RPC."
+          );
+          throw err;
+        }
+
+        if (
+          err.walk((e) => e instanceof FeeCapTooLowError) ||
+          err.walk(
+            (e) =>
+              e instanceof InternalRpcError &&
+              e.details.includes("replacement transaction underpriced")
+          )
+        ) {
+          this.logger.warn(
+            "The gas price of the transaction is too low or there is an existing transaction with higher gas with the same nonce. " +
+              "The price will be increased in the next push. Skipping this push. " +
+              "If this keeps happening or transactions are not landing you need to increase the override gas price " +
+              "multiplier and the cap to increase the likelihood of the transaction landing on-chain."
           );
           return;
         }
 
+        if (
+          err.walk(
+            (e) =>
+              e instanceof TransactionExecutionError &&
+              (e.details.includes("nonce too low") ||
+                e.message.includes("Nonce provided for the transaction"))
+          )
+        ) {
+          this.logger.info(
+            "The nonce is incorrect. This is an expected behaviour in high frequency or multi-instance setup. Skipping this push."
+          );
+          return;
+        }
+
+        // We normally crash on unknown failures but we believe that this type of error is safe to skip. The other reason is that
+        // wometimes we see a TransactionExecutionError because of the nonce without any details and it is not catchable.
+        if (err.walk((e) => e instanceof TransactionExecutionError)) {
+          this.logger.error(
+            { err },
+            "Transaction execution failed. This is an expected behaviour in high frequency or multi-instance setup. " +
+              "Please review this error and file an issue if it is a bug. Skipping this push."
+          );
+          return;
+        }
+
+        // The following errors are part of the legacy code and might not work as expected.
+        // We are keeping them in case they help with handling what is not covered above.
         if (
           err.message.includes("the tx doesn't have the correct nonce.") ||
           err.message.includes("nonce too low") ||
           err.message.includes("invalid nonce")
         ) {
           this.logger.info(
-            { err, receipt },
             "The nonce is incorrect (are multiple users using this account?). Skipping this push."
           );
           return;
@@ -269,9 +335,9 @@ export class EvmPricePusher implements IPricePusher {
           // LastPushAttempt was stored with the class
           // Next time the update will be executing, it will check the last attempt
           // and increase the gas price accordingly.
-          this.logger.info(
-            { err, receipt },
-            "The transaction failed with error: max fee per gas less than block base fee "
+          this.logger.warn(
+            "The transaction failed with error: max fee per gas less than block base fee. " +
+              "The fee will be increased in the next push. Skipping this push."
           );
           return;
         }
@@ -279,37 +345,26 @@ export class EvmPricePusher implements IPricePusher {
         if (
           err.message.includes("sender doesn't have enough funds to send tx.")
         ) {
-          this.logger.error(
-            { err, receipt },
-            "Payer is out of balance, please top it up."
-          );
-          throw err;
-        }
-
-        if (err.message.includes("transaction underpriced")) {
-          this.logger.error(
-            { err, receipt },
-            "The gas price of the transaction is too low. Skipping this push. " +
-              "You might want to use a custom gas station or increase the override gas price " +
-              "multiplier to increase the likelihood of the transaction landing on-chain."
-          );
-          return;
+          this.logger.error("Payer is out of balance, please top it up.");
+          throw new Error("Please top up the wallet");
         }
 
         if (err.message.includes("could not replace existing tx")) {
           this.logger.error(
-            { err, receipt },
-            "A transaction with the same nonce has been mined and this one is no longer needed."
+            "A transaction with the same nonce has been mined and this one is no longer needed. Skipping this push."
           );
           return;
         }
+      }
 
-        this.logger.error(
-          { err, receipt },
-          "An unidentified error has occured."
-        );
-        throw err;
-      });
+      // If the error is not handled, we will crash the process.
+      this.logger.error(
+        { err },
+        "The transaction failed with an unhandled error. crashing the process. " +
+          "Please review this error and file an issue if it is a bug."
+      );
+      throw err;
+    }
 
     // Update lastAttempt
     this.lastPushAttempt = {
@@ -318,6 +373,29 @@ export class EvmPricePusher implements IPricePusher {
     };
   }
 
+  private async waitForTransactionReceipt(hash: `0x${string}`): Promise<void> {
+    try {
+      const receipt = await this.client.waitForTransactionReceipt({
+        hash: hash,
+      });
+
+      switch (receipt.status) {
+        case "success":
+          this.logger.debug({ hash, receipt }, "Price update successful");
+          this.logger.info({ hash }, "Price update successful");
+          break;
+        default:
+          this.logger.info(
+            { hash, receipt },
+            "Price update did not succeed or its transaction did not land. " +
+              "This is an expected behaviour in high frequency or multi-instance setup."
+          );
+      }
+    } catch (err: any) {
+      this.logger.warn({ err }, "Failed to get transaction receipt");
+    }
+  }
+
   private async getPriceFeedsUpdateData(
     priceIds: HexString[]
   ): Promise<string[]> {
@@ -327,86 +405,3 @@ export class EvmPricePusher implements IPricePusher {
     );
   }
 }
-
-export class PythContractFactory {
-  constructor(
-    private endpoint: string,
-    private mnemonic: string,
-    private pythContractAddress: string
-  ) {}
-
-  /**
-   * This method creates a web3 Pyth contract with payer (based on HDWalletProvider). As this
-   * provider is an HDWalletProvider it does not support subscriptions even if the
-   * endpoint is a websocket endpoint.
-   *
-   * @returns Pyth contract
-   */
-  createPythContractWithPayer(): Contract {
-    const provider = this.createWeb3PayerProvider();
-
-    const web3 = new Web3(provider as any);
-
-    return new web3.eth.Contract(
-      AbstractPythAbi as any,
-      this.pythContractAddress,
-      {
-        from: provider.getAddress(0),
-      }
-    );
-  }
-
-  /**
-   * This method creates a web3 Pyth contract with the given endpoint as its provider. If
-   * the endpoint is a websocket endpoint the contract will support subscriptions.
-   *
-   * @returns Pyth contract
-   */
-  createPythContract(): Contract {
-    const provider = this.createWeb3Provider();
-    const web3 = new Web3(provider);
-    return new web3.eth.Contract(
-      AbstractPythAbi as any,
-      this.pythContractAddress
-    );
-  }
-
-  hasWebsocketProvider(): boolean {
-    return isWsEndpoint(this.endpoint);
-  }
-
-  createWeb3Provider(): HttpProvider | WebsocketProvider {
-    if (isWsEndpoint(this.endpoint)) {
-      Web3.providers.WebsocketProvider.prototype.sendAsync =
-        Web3.providers.WebsocketProvider.prototype.send;
-      return new Web3.providers.WebsocketProvider(this.endpoint, {
-        clientConfig: {
-          keepalive: true,
-          keepaliveInterval: 30000,
-        },
-        reconnect: {
-          auto: true,
-          delay: 1000,
-          onTimeout: true,
-        },
-        timeout: 30000,
-      });
-    } else {
-      Web3.providers.HttpProvider.prototype.sendAsync =
-        Web3.providers.HttpProvider.prototype.send;
-      return new Web3.providers.HttpProvider(this.endpoint, {
-        keepAlive: true,
-        timeout: 30000,
-      });
-    }
-  }
-
-  createWeb3PayerProvider() {
-    return new HDWalletProvider({
-      mnemonic: {
-        phrase: this.mnemonic,
-      },
-      providerOrUrl: this.createWeb3Provider() as ProviderOrUrl,
-    });
-  }
-}

+ 660 - 0
apps/price_pusher/src/evm/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;

+ 18 - 0
apps/price_pusher/src/evm/pyth-contract.ts

@@ -0,0 +1,18 @@
+import { getContract, Address, GetContractReturnType } from "viem";
+import { PythAbi } from "./pyth-abi";
+import { SuperWalletClient } from "./super-wallet";
+
+export type PythContract = GetContractReturnType<
+  typeof PythAbi,
+  SuperWalletClient
+>;
+
+export const createPythContract = (
+  client: SuperWalletClient,
+  address: Address
+): PythContract =>
+  getContract({
+    client,
+    abi: PythAbi,
+    address,
+  });

+ 71 - 0
apps/price_pusher/src/evm/super-wallet.ts

@@ -0,0 +1,71 @@
+import {
+  createPublicClient,
+  createWalletClient,
+  defineChain,
+  http,
+  webSocket,
+  Account,
+  Chain,
+  publicActions,
+  Client,
+  RpcSchema,
+  WalletActions,
+  PublicActions,
+  WebSocketTransport,
+  HttpTransport,
+  Transport,
+} from "viem";
+import { mnemonicToAccount } from "viem/accounts";
+import * as chains from "viem/chains";
+import { isWsEndpoint } from "../utils";
+
+const UNKNOWN_CHAIN_CONFIG = {
+  name: "Unknown",
+  nativeCurrency: {
+    name: "Unknown",
+    symbol: "Unknown",
+    decimals: 18,
+  },
+  rpcUrls: {
+    default: {
+      http: [],
+    },
+  },
+};
+
+export type SuperWalletClient = Client<
+  Transport,
+  Chain,
+  Account,
+  RpcSchema,
+  PublicActions<Transport, Chain, Account> & WalletActions<Chain, Account>
+>;
+
+// Get the transport based on the endpoint
+const getTransport = (endpoint: string): WebSocketTransport | HttpTransport =>
+  isWsEndpoint(endpoint) ? webSocket(endpoint) : http(endpoint);
+
+// Get the chain corresponding to the chainId. If the chain is not found, it will return
+// an unknown chain which should work fine in most of the cases. We might need to update
+// the viem package to support new chains if they don't work as expected with the unknown
+// chain.
+const getChainById = (chainId: number): Chain =>
+  Object.values(chains).find((chain) => chain.id === chainId) ||
+  defineChain({ id: chainId, ...UNKNOWN_CHAIN_CONFIG });
+
+export const createClient = async (
+  endpoint: string,
+  mnemonic: string
+): Promise<SuperWalletClient> => {
+  const transport = getTransport(endpoint);
+
+  const chainId = await createPublicClient({
+    transport,
+  }).getChainId();
+
+  return createWalletClient({
+    transport,
+    account: mnemonicToAccount(mnemonic),
+    chain: getChainById(chainId),
+  }).extend(publicActions);
+};

+ 13 - 6
apps/price_pusher/src/utils.ts

@@ -18,12 +18,11 @@ export function removeLeading0x(id: HexString): HexString {
   return id;
 }
 
-export function addLeading0x(id: HexString): HexString {
-  if (id.startsWith("0x")) {
-    return id;
-  }
-  return "0x" + id;
-}
+export const addLeading0x = (id: HexString): `0x${string}` =>
+  hasLeading0x(id) ? id : `0x${id}`;
+
+const hasLeading0x = (input: string): input is `0x${string}` =>
+  input.startsWith("0x");
 
 export function isWsEndpoint(endpoint: string): boolean {
   const url = new URL(endpoint);
@@ -47,3 +46,11 @@ export function verifyValidOption<
     option + " is not a valid option. Please choose between " + validOptions;
   throw new Error(errorString);
 }
+
+export const assertDefined = <T>(value: T | undefined): T => {
+  if (value === undefined) {
+    throw new Error("Assertion failed: value was undefined");
+  } else {
+    return value;
+  }
+};

+ 63 - 22
pnpm-lock.yaml

@@ -251,9 +251,6 @@ importers:
       '@solana/web3.js':
         specifier: 1.92.3
         version: 1.92.3(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
-      '@truffle/hdwallet-provider':
-        specifier: ^2.1.3
-        version: 2.1.5(@babel/core@7.24.7)(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
       '@types/pino':
         specifier: ^7.0.5
         version: 7.0.5
@@ -272,15 +269,9 @@ importers:
       pino:
         specifier: ^9.2.0
         version: 9.2.0
-      web3:
-        specifier: ^1.8.1
-        version: 1.8.2(bufferutil@4.0.8)(encoding@0.1.13)(utf-8-validate@5.0.10)
-      web3-core:
-        specifier: ^1.8.1
-        version: 1.10.0(encoding@0.1.13)
-      web3-eth-contract:
-        specifier: ^1.8.1
-        version: 1.8.2(encoding@0.1.13)
+      viem:
+        specifier: ^2.19.4
+        version: 2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8)
       yaml:
         specifier: ^2.1.1
         version: 2.4.3
@@ -8350,6 +8341,17 @@ packages:
       zod:
         optional: true
 
+  abitype@1.0.5:
+    resolution: {integrity: sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw==}
+    peerDependencies:
+      typescript: '>=5.0.4'
+      zod: ^3 >=3.22.0
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+      zod:
+        optional: true
+
   abort-controller@3.0.0:
     resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
     engines: {node: '>=6.5'}
@@ -18105,6 +18107,14 @@ packages:
       typescript:
         optional: true
 
+  viem@2.19.4:
+    resolution: {integrity: sha512-JdhK3ui3uPD2tnpqGNkJaDQV4zTfOeKXcF+VrU8RG88Dn2e0lFjv6l7m0YNmYLsHm+n5vFFfCLcUrTk6xcYv5w==}
+    peerDependencies:
+      typescript: '>=5.0.4'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
   viem@2.8.13:
     resolution: {integrity: sha512-jEbRUjsiBwmoDr3fnKL1Bh1GhK5ERhmZcPLeARtEaQoBTPB6bcO2siKhNPVOF8qrYRnGHGQrZHncBWMQhTjGYg==}
     peerDependencies:
@@ -18611,6 +18621,9 @@ packages:
     resolution: {integrity: sha512-kQSF2NlHk8yjS3SRiJW3S+U5ibkEmVRhB4/GYsVwGvdAkFC2b+EIE1Ob7J56OmqW9VBZgkx1+SuWqo5JTIJSYQ==}
     engines: {node: '>=14.0.0', npm: '>=6.12.0'}
 
+  webauthn-p256@0.0.5:
+    resolution: {integrity: sha512-drMGNWKdaixZNobeORVIqq7k5DsRC9FnG201K2QjeOoQLmtSDaSsVZdkg6n5jUALJKcAG++zBPJXmv6hy0nWFg==}
+
   webextension-polyfill@0.10.0:
     resolution: {integrity: sha512-c5s35LgVa5tFaHhrZDnr3FpQpjj1BB+RXhLTYUxGqBVN460HkbM8TBtEqdXWbpTKfzwCcjAZVF7zXCYSKtcp9g==}
 
@@ -23699,7 +23712,7 @@ snapshots:
       '@fuel-ts/utils': 0.89.2
       '@fuel-ts/versions': 0.89.2
       '@fuels/vm-asm': 0.50.0
-      '@noble/curves': 1.4.0
+      '@noble/curves': 1.4.2
       events: 3.3.0
       graphql: 16.8.2
       graphql-request: 5.0.0(encoding@0.1.13)(graphql@16.8.2)
@@ -26547,10 +26560,10 @@ snapshots:
   '@mysten/sui.js@0.32.2(bufferutil@4.0.7)(utf-8-validate@6.0.3)':
     dependencies:
       '@mysten/bcs': 0.7.1
-      '@noble/curves': 1.4.0
+      '@noble/curves': 1.4.2
       '@noble/hashes': 1.4.0
       '@scure/bip32': 1.4.0
-      '@scure/bip39': 1.2.2
+      '@scure/bip39': 1.3.0
       '@suchipi/femver': 1.0.0
       jayson: 4.1.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
       rpc-websockets: 7.5.1
@@ -26563,10 +26576,10 @@ snapshots:
   '@mysten/sui.js@0.32.2(bufferutil@4.0.8)(utf-8-validate@5.0.10)':
     dependencies:
       '@mysten/bcs': 0.7.1
-      '@noble/curves': 1.4.0
+      '@noble/curves': 1.4.2
       '@noble/hashes': 1.4.0
       '@scure/bip32': 1.4.0
-      '@scure/bip39': 1.2.2
+      '@scure/bip39': 1.3.0
       '@suchipi/femver': 1.0.0
       jayson: 4.1.0(bufferutil@4.0.8)(utf-8-validate@5.0.10)
       rpc-websockets: 7.5.1
@@ -26579,10 +26592,10 @@ snapshots:
   '@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.4.0
+      '@noble/curves': 1.4.2
       '@noble/hashes': 1.4.0
       '@scure/bip32': 1.4.0
-      '@scure/bip39': 1.2.2
+      '@scure/bip39': 1.3.0
       '@suchipi/femver': 1.0.0
       jayson: 4.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
       rpc-websockets: 7.5.1
@@ -28267,9 +28280,9 @@ snapshots:
 
   '@scure/bip32@1.4.0':
     dependencies:
-      '@noble/curves': 1.4.0
+      '@noble/curves': 1.4.2
       '@noble/hashes': 1.4.0
-      '@scure/base': 1.1.6
+      '@scure/base': 1.1.7
 
   '@scure/bip39@1.1.0':
     dependencies:
@@ -28289,7 +28302,7 @@ snapshots:
   '@scure/bip39@1.2.2':
     dependencies:
       '@noble/hashes': 1.3.3
-      '@scure/base': 1.1.6
+      '@scure/base': 1.1.7
 
   '@scure/bip39@1.3.0':
     dependencies:
@@ -32064,6 +32077,11 @@ snapshots:
       typescript: 5.5.2
       zod: 3.23.8
 
+  abitype@1.0.5(typescript@5.4.5)(zod@3.23.8):
+    optionalDependencies:
+      typescript: 5.4.5
+      zod: 3.23.8
+
   abort-controller@3.0.0:
     dependencies:
       event-target-shim: 5.0.1
@@ -46225,6 +46243,24 @@ snapshots:
       - utf-8-validate
       - zod
 
+  viem@2.19.4(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@5.0.10)(zod@3.23.8):
+    dependencies:
+      '@adraffy/ens-normalize': 1.10.0
+      '@noble/curves': 1.4.0
+      '@noble/hashes': 1.4.0
+      '@scure/bip32': 1.4.0
+      '@scure/bip39': 1.3.0
+      abitype: 1.0.5(typescript@5.4.5)(zod@3.23.8)
+      isows: 1.0.4(ws@8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10))
+      webauthn-p256: 0.0.5
+      ws: 8.17.1(bufferutil@4.0.8)(utf-8-validate@5.0.10)
+    optionalDependencies:
+      typescript: 5.4.5
+    transitivePeerDependencies:
+      - bufferutil
+      - utf-8-validate
+      - zod
+
   viem@2.8.13(bufferutil@4.0.8)(typescript@5.4.5)(utf-8-validate@6.0.4)(zod@3.23.8):
     dependencies:
       '@adraffy/ens-normalize': 1.10.0
@@ -47488,6 +47524,11 @@ snapshots:
       - utf-8-validate
       - zod
 
+  webauthn-p256@0.0.5:
+    dependencies:
+      '@noble/curves': 1.4.2
+      '@noble/hashes': 1.4.0
+
   webextension-polyfill@0.10.0: {}
 
   webidl-conversions@3.0.1: {}