瀏覽代碼

move changes from pyth-js (#624)

Dev Kalra 2 年之前
父節點
當前提交
ae88640422

+ 1 - 1
price_pusher/README.md

@@ -122,4 +122,4 @@ docker-compose -f docker-compose.testnet.sample.yaml up
 
 It will take a few minutes until all the services are up and running.
 
-[pyth price service]: https://github.com/pyth-network/pyth-crosschain/tree/main/third_party/pyth/price-service
+[pyth price service]: https://github.com/pyth-network/pyth-crosschain/tree/main/price_service/server

+ 2 - 2
price_pusher/docker-compose.mainnet.sample.yaml

@@ -1,7 +1,7 @@
 services:
   spy:
     # Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand
-    image: ghcr.io/wormhole-foundation/guardiand:v.2.14.5
+    image: ghcr.io/wormhole-foundation/guardiand:v.2.14.8.1
     command:
       - "spy"
       - "--nodeKey"
@@ -16,7 +16,7 @@ services:
       - "warn"
   price-service:
     # Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
-    image: public.ecr.aws/pyth-network/xc-server:v2.2.3
+    image: public.ecr.aws/pyth-network/xc-server:v3.0.0
     environment:
       SPY_SERVICE_HOST: "spy:7072"
       SPY_SERVICE_FILTERS: |

+ 2 - 2
price_pusher/docker-compose.testnet.sample.yaml

@@ -1,7 +1,7 @@
 services:
   spy:
     # Find latest Guardian images in https://github.com/wormhole-foundation/wormhole/pkgs/container/guardiand
-    image: ghcr.io/wormhole-foundation/guardiand:v.2.14.5
+    image: ghcr.io/wormhole-foundation/guardiand:v.2.14.8.1
     command:
       - "spy"
       - "--nodeKey"
@@ -16,7 +16,7 @@ services:
       - "warn"
   price-service:
     # Find latest price service images https://gallery.ecr.aws/pyth-network/xc-server
-    image: public.ecr.aws/pyth-network/xc-server:v2.2.3
+    image: public.ecr.aws/pyth-network/xc-server:v3.0.0
     environment:
       SPY_SERVICE_HOST: "spy:7072"
       SPY_SERVICE_FILTERS: |

+ 3 - 2
price_pusher/package-lock.json

@@ -1,14 +1,15 @@
 {
   "name": "@pythnetwork/pyth-evm-price-pusher",
-  "version": "2.0.0",
+  "version": "2.0.1",
   "lockfileVersion": 2,
   "requires": true,
   "packages": {
     "": {
       "name": "@pythnetwork/pyth-evm-price-pusher",
-      "version": "2.0.0",
+      "version": "2.0.1",
       "license": "Apache-2.0",
       "dependencies": {
+        "@pythnetwork/pyth-common-js": "^1.2.0",
         "@pythnetwork/pyth-evm-js": "^1.1.0",
         "@pythnetwork/pyth-sdk-solidity": "^2.2.0",
         "@truffle/hdwallet-provider": "^2.1.3",

+ 2 - 1
price_pusher/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/pyth-evm-price-pusher",
-  "version": "2.0.0",
+  "version": "2.0.1",
   "description": "Pyth EVM Price Pusher",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",
@@ -43,6 +43,7 @@
     "typescript": "^4.6.3"
   },
   "dependencies": {
+    "@pythnetwork/pyth-common-js": "^1.2.0",
     "@pythnetwork/pyth-evm-js": "^1.1.0",
     "@pythnetwork/pyth-sdk-solidity": "^2.2.0",
     "@truffle/hdwallet-provider": "^2.1.3",

+ 44 - 0
price_pusher/src/controller.ts

@@ -0,0 +1,44 @@
+import { UnixTimestamp } from "@pythnetwork/pyth-evm-js";
+import { DurationInSeconds, sleep } from "./utils";
+import { ChainPricePusher, PriceListener } from "./interface";
+import { PriceConfig, shouldUpdate } from "./price-config";
+
+export class Controller {
+  private cooldownDuration: DurationInSeconds;
+  constructor(
+    private priceConfigs: PriceConfig[],
+    private sourcePriceListener: PriceListener,
+    private targetPriceListener: PriceListener,
+    private targetChainPricePusher: ChainPricePusher,
+    config: {
+      cooldownDuration: DurationInSeconds;
+    }
+  ) {
+    this.cooldownDuration = config.cooldownDuration;
+  }
+
+  async start() {
+    for (;;) {
+      const pricesToPush: PriceConfig[] = [];
+      const pubTimesToPush: UnixTimestamp[] = [];
+
+      for (const priceConfig of this.priceConfigs) {
+        const priceId = priceConfig.id;
+
+        const targetLatestPrice =
+          this.targetPriceListener.getLatestPriceInfo(priceId);
+        const sourceLatestPrice =
+          this.sourcePriceListener.getLatestPriceInfo(priceId);
+
+        if (shouldUpdate(priceConfig, sourceLatestPrice, targetLatestPrice)) {
+          pricesToPush.push(priceConfig);
+          pubTimesToPush.push((targetLatestPrice?.publishTime || 0) + 1);
+        }
+      }
+      // note that the priceIds are without leading "0x"
+      const priceIds = pricesToPush.map((priceConfig) => priceConfig.id);
+      this.targetChainPricePusher.updatePriceFeed(priceIds, pubTimesToPush);
+      await sleep(this.cooldownDuration * 1000);
+    }
+  }
+}

+ 0 - 141
price_pusher/src/evm-price-listener.ts

@@ -1,141 +0,0 @@
-import { HexString } from "@pythnetwork/pyth-evm-js";
-import { Contract, EventData } from "web3-eth-contract";
-import { PriceConfig } from "./price-config";
-import { PriceInfo, PriceListener } from "./price-listener";
-import { PythContractFactory } from "./pyth-contract-factory";
-import { addLeading0x, DurationInSeconds, removeLeading0x } from "./utils";
-
-export class EvmPriceListener implements PriceListener {
-  private pythContractFactory: PythContractFactory;
-  private pythContract: Contract;
-  private latestPriceInfo: Map<HexString, PriceInfo>;
-  private priceIds: HexString[];
-  private priceIdToAlias: Map<HexString, string>;
-
-  private pollingFrequency: DurationInSeconds;
-
-  constructor(
-    pythContractFactory: PythContractFactory,
-    priceConfigs: PriceConfig[],
-    config: {
-      pollingFrequency: DurationInSeconds;
-    }
-  ) {
-    this.latestPriceInfo = new Map();
-    this.priceIds = priceConfigs.map((priceConfig) => priceConfig.id);
-    this.priceIdToAlias = new Map(
-      priceConfigs.map((priceConfig) => [priceConfig.id, priceConfig.alias])
-    );
-
-    this.pollingFrequency = config.pollingFrequency;
-
-    this.pythContractFactory = pythContractFactory;
-    this.pythContract = this.pythContractFactory.createPythContract();
-  }
-
-  // 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()) {
-      console.log("Subscribing to the target network pyth contract events...");
-      this.startSubscription();
-    } else {
-      console.log(
-        "The target network RPC endpoint is not Websocket. " +
-          "Listening for updates only via polling...."
-      );
-    }
-
-    console.log(`Polling the prices every ${this.pollingFrequency} seconds...`);
-    setInterval(this.pollPrices.bind(this), this.pollingFrequency * 1000);
-
-    await this.pollPrices();
-  }
-
-  private async startSubscription() {
-    for (const priceId of this.priceIds) {
-      this.pythContract.events.PriceFeedUpdate(
-        {
-          filter: {
-            id: addLeading0x(priceId),
-            fresh: true,
-          },
-        },
-        this.onPriceFeedUpdate.bind(this)
-      );
-    }
-  }
-
-  private onPriceFeedUpdate(err: Error | null, event: EventData) {
-    if (err !== null) {
-      console.error("PriceFeedUpdate EventEmitter received an error..");
-      throw err;
-    }
-
-    const priceId = removeLeading0x(event.returnValues.id);
-    console.log(
-      `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);
-  }
-
-  private async pollPrices() {
-    console.log("Polling evm prices...");
-    for (const priceId of this.priceIds) {
-      const currentPriceInfo = await this.getOnChainPriceInfo(priceId);
-      if (currentPriceInfo !== undefined) {
-        this.updateLatestPriceInfo(priceId, currentPriceInfo);
-      }
-    }
-  }
-
-  getLatestPriceInfo(priceId: string): PriceInfo | undefined {
-    return this.latestPriceInfo.get(priceId);
-  }
-
-  async getOnChainPriceInfo(
-    priceId: HexString
-  ): Promise<PriceInfo | undefined> {
-    let priceRaw;
-    try {
-      priceRaw = await this.pythContract.methods
-        .getPriceUnsafe(addLeading0x(priceId))
-        .call();
-    } catch (e) {
-      console.error(`Getting on-chain price for ${priceId} failed. Error:`);
-      console.error(e);
-      return undefined;
-    }
-
-    return {
-      conf: priceRaw.conf,
-      price: priceRaw.price,
-      publishTime: Number(priceRaw.publishTime),
-    };
-  }
-
-  private updateLatestPriceInfo(priceId: HexString, observedPrice: PriceInfo) {
-    const cachedLatestPriceInfo = this.getLatestPriceInfo(priceId);
-
-    // Ignore the observed price if the cache already has newer
-    // price. This could happen because we are using polling and
-    // subscription at the same time.
-    if (
-      cachedLatestPriceInfo !== undefined &&
-      cachedLatestPriceInfo.publishTime > observedPrice.publishTime
-    ) {
-      return;
-    }
-
-    this.latestPriceInfo.set(priceId, observedPrice);
-  }
-}

+ 312 - 0
price_pusher/src/evm.ts

@@ -0,0 +1,312 @@
+import {
+  EvmPriceServiceConnection,
+  HexString,
+  UnixTimestamp,
+} from "@pythnetwork/pyth-evm-js";
+import { Contract, EventData } from "web3-eth-contract";
+import { PriceConfig } from "./price-config";
+import { ChainPricePusher, PriceInfo, PriceListener } 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 { Provider } from "web3/providers";
+import Web3 from "web3";
+import { isWsEndpoint } from "./utils";
+
+export class EvmPriceListener implements PriceListener {
+  private pythContractFactory: PythContractFactory;
+  private pythContract: Contract;
+  private latestPriceInfo: Map<HexString, PriceInfo>;
+  private priceIds: HexString[];
+  private priceIdToAlias: Map<HexString, string>;
+
+  private pollingFrequency: DurationInSeconds;
+
+  constructor(
+    pythContractFactory: PythContractFactory,
+    priceConfigs: PriceConfig[],
+    config: {
+      pollingFrequency: DurationInSeconds;
+    }
+  ) {
+    this.latestPriceInfo = new Map();
+    this.priceIds = priceConfigs.map((priceConfig) => priceConfig.id);
+    this.priceIdToAlias = new Map(
+      priceConfigs.map((priceConfig) => [priceConfig.id, priceConfig.alias])
+    );
+
+    this.pollingFrequency = config.pollingFrequency;
+
+    this.pythContractFactory = pythContractFactory;
+    this.pythContract = this.pythContractFactory.createPythContract();
+  }
+
+  // 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()) {
+      console.log("Subscribing to the target network pyth contract events...");
+      this.startSubscription();
+    } else {
+      console.log(
+        "The target network RPC endpoint is not Websocket. " +
+          "Listening for updates only via polling...."
+      );
+    }
+
+    console.log(`Polling the prices every ${this.pollingFrequency} seconds...`);
+    setInterval(this.pollPrices.bind(this), this.pollingFrequency * 1000);
+
+    await this.pollPrices();
+  }
+
+  private async startSubscription() {
+    for (const priceId of this.priceIds) {
+      this.pythContract.events.PriceFeedUpdate(
+        {
+          filter: {
+            id: addLeading0x(priceId),
+            fresh: true,
+          },
+        },
+        this.onPriceFeedUpdate.bind(this)
+      );
+    }
+  }
+
+  private onPriceFeedUpdate(err: Error | null, event: EventData) {
+    if (err !== null) {
+      console.error("PriceFeedUpdate EventEmitter received an error..");
+      throw err;
+    }
+
+    const priceId = removeLeading0x(event.returnValues.id);
+    console.log(
+      `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);
+  }
+
+  private async pollPrices() {
+    console.log("Polling evm prices...");
+    for (const priceId of this.priceIds) {
+      const currentPriceInfo = await this.getOnChainPriceInfo(priceId);
+      if (currentPriceInfo !== undefined) {
+        this.updateLatestPriceInfo(priceId, currentPriceInfo);
+      }
+    }
+  }
+
+  getLatestPriceInfo(priceId: string): PriceInfo | undefined {
+    return this.latestPriceInfo.get(priceId);
+  }
+
+  async getOnChainPriceInfo(
+    priceId: HexString
+  ): Promise<PriceInfo | undefined> {
+    let priceRaw;
+    try {
+      priceRaw = await this.pythContract.methods
+        .getPriceUnsafe(addLeading0x(priceId))
+        .call();
+    } catch (e) {
+      console.error(`Getting on-chain price for ${priceId} failed. Error:`);
+      console.error(e);
+      return undefined;
+    }
+
+    return {
+      conf: priceRaw.conf,
+      price: priceRaw.price,
+      publishTime: Number(priceRaw.publishTime),
+    };
+  }
+
+  private updateLatestPriceInfo(priceId: HexString, observedPrice: PriceInfo) {
+    const cachedLatestPriceInfo = this.getLatestPriceInfo(priceId);
+
+    // Ignore the observed price if the cache already has newer
+    // price. This could happen because we are using polling and
+    // subscription at the same time.
+    if (
+      cachedLatestPriceInfo !== undefined &&
+      cachedLatestPriceInfo.publishTime > observedPrice.publishTime
+    ) {
+      return;
+    }
+
+    this.latestPriceInfo.set(priceId, observedPrice);
+  }
+}
+
+export class EvmPricePusher implements ChainPricePusher {
+  constructor(
+    private connection: EvmPriceServiceConnection,
+    private pythContract: Contract
+  ) {}
+  // 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)
+  // and will help multiple price pushers to have consistent behaviour.
+  async updatePriceFeed(
+    priceIds: string[],
+    pubTimesToPush: UnixTimestamp[]
+  ): Promise<void> {
+    if (priceIds.length === 0) {
+      return;
+    }
+
+    if (priceIds.length !== pubTimesToPush.length)
+      throw new Error("Invalid arguments");
+
+    const priceIdsWith0x = priceIds.map((priceId) => addLeading0x(priceId));
+
+    const priceFeedUpdateData = await this.connection.getPriceFeedsUpdateData(
+      priceIdsWith0x
+    );
+
+    console.log(
+      "Pushing ",
+      priceIdsWith0x.map((priceIdWith0x) => `${priceIdWith0x}`)
+    );
+
+    const updateFee = await this.pythContract.methods
+      .getUpdateFee(priceFeedUpdateData)
+      .call();
+    console.log(`Update fee: ${updateFee}`);
+
+    this.pythContract.methods
+      .updatePriceFeedsIfNecessary(
+        priceFeedUpdateData,
+        priceIdsWith0x,
+        pubTimesToPush
+      )
+      .send({ value: updateFee })
+      .on("transactionHash", (hash: string) => {
+        console.log(`Successful. Tx hash: ${hash}`);
+      })
+      .on("error", (err: Error, receipt?: TransactionReceipt) => {
+        if (
+          err.message.includes(
+            "VM Exception while processing transaction: 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.
+          console.log(
+            "Execution reverted. With high probablity, the target chain price " +
+              "has already updated, Skipping this push."
+          );
+          return;
+        }
+
+        if (
+          err.message.includes("the tx doesn't have the correct nonce.") ||
+          err.message.includes("nonce too low")
+        ) {
+          console.log(
+            "Multiple users are using the same accounts and nonce is incorrect. Skipping this push."
+          );
+          return;
+        }
+
+        if (
+          err.message.includes("sender doesn't have enough funds to send tx.")
+        ) {
+          console.error("Payer is out of balance, please top it up.");
+          throw err;
+        }
+
+        console.error("An unidentified error has occured:");
+        console.error(receipt);
+        throw err;
+      });
+  }
+}
+
+export class PythContractFactory {
+  constructor(
+    private endpoint: string,
+    private mnemonic: string,
+    private pythContractAddr: 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 = new HDWalletProvider({
+      mnemonic: {
+        phrase: this.mnemonic,
+      },
+      providerOrUrl: this.createWeb3Provider() as Provider,
+    });
+
+    const web3 = new Web3(provider as any);
+
+    return new web3.eth.Contract(
+      AbstractPythAbi as any,
+      this.pythContractAddr,
+      {
+        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.pythContractAddr);
+  }
+
+  hasWebsocketProvider(): boolean {
+    return isWsEndpoint(this.endpoint);
+  }
+
+  private createWeb3Provider() {
+    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,
+      });
+    }
+  }
+}

+ 10 - 7
price_pusher/src/index.ts

@@ -6,12 +6,11 @@ import {
   EvmPriceServiceConnection,
   CONTRACT_ADDR,
 } from "@pythnetwork/pyth-evm-js";
-import { Pusher } from "./pusher";
-import { EvmPriceListener } from "./evm-price-listener";
+import { Controller } from "./controller";
+import { EvmPriceListener, EvmPricePusher, PythContractFactory } from "./evm";
 import { PythPriceListener } from "./pyth-price-listener";
 import fs from "fs";
 import { readPriceConfigFile } from "./price-config";
-import { PythContractFactory } from "./pyth-contract-factory";
 
 const argv = yargs(hideBin(process.argv))
   .option("evm-endpoint", {
@@ -101,12 +100,16 @@ async function run() {
 
   const pythPriceListener = new PythPriceListener(connection, priceConfigs);
 
-  const handler = new Pusher(
+  const evmPricePusher = new EvmPricePusher(
     connection,
-    pythContractFactory,
-    evmPriceListener,
-    pythPriceListener,
+    pythContractFactory.createPythContractWithPayer()
+  );
+
+  const handler = new Controller(
     priceConfigs,
+    pythPriceListener,
+    evmPriceListener,
+    evmPricePusher,
     {
       cooldownDuration: argv.cooldownDuration,
     }

+ 7 - 0
price_pusher/src/price-listener.ts → price_pusher/src/interface.ts

@@ -10,3 +10,10 @@ export interface PriceListener {
   // Should return undefined only when the price does not exist.
   getLatestPriceInfo(priceId: HexString): undefined | PriceInfo;
 }
+
+export interface ChainPricePusher {
+  updatePriceFeed(
+    priceIds: string[],
+    pubTimesToPush: UnixTimestamp[]
+  ): Promise<void>;
+}

+ 82 - 0
price_pusher/src/price-config.ts

@@ -3,6 +3,7 @@ import Joi from "joi";
 import YAML from "yaml";
 import fs from "fs";
 import { DurationInSeconds, PctNumber, removeLeading0x } from "./utils";
+import { PriceInfo } from "./interface";
 
 const PriceConfigFileSchema: Joi.Schema = Joi.array()
   .items(
@@ -47,3 +48,84 @@ export function readPriceConfigFile(path: string): PriceConfig[] {
     return priceConfig;
   });
 }
+
+/**
+ * Checks whether on-chain price needs to be updated with the latest pyth price information.
+ *
+ * @param priceConfig Config of the price feed to check
+ * @returns True if the on-chain price needs to be updated.
+ */
+export function shouldUpdate(
+  priceConfig: PriceConfig,
+  sourceLatestPrice: PriceInfo | undefined,
+  targetLatestPrice: PriceInfo | undefined
+): boolean {
+  const priceId = priceConfig.id;
+
+  // There is no price to update the target with.
+  if (sourceLatestPrice === undefined) {
+    return false;
+  }
+
+  // It means that price never existed there. So we should push the latest price feed.
+  if (targetLatestPrice === undefined) {
+    console.log(
+      `${priceConfig.alias} (${priceId}) is not available on the target network. Pushing the price.`
+    );
+    return true;
+  }
+
+  // The current price is not newer than the price onchain
+  if (sourceLatestPrice.publishTime < targetLatestPrice.publishTime) {
+    return false;
+  }
+
+  const timeDifference =
+    sourceLatestPrice.publishTime - targetLatestPrice.publishTime;
+
+  const priceDeviationPct =
+    (Math.abs(
+      Number(sourceLatestPrice.price) - Number(targetLatestPrice.price)
+    ) /
+      Number(targetLatestPrice.price)) *
+    100;
+  const confidenceRatioPct = Math.abs(
+    (Number(sourceLatestPrice.conf) / Number(sourceLatestPrice.price)) * 100
+  );
+
+  console.log(`Analyzing price ${priceConfig.alias} (${priceId})`);
+
+  console.log("Source latest price: ", sourceLatestPrice);
+  console.log("Target latest price: ", targetLatestPrice);
+
+  console.log(
+    `Time difference: ${timeDifference} (< ${priceConfig.timeDifference}?)`
+  );
+  console.log(
+    `Price deviation: ${priceDeviationPct.toFixed(5)}% (< ${
+      priceConfig.priceDeviation
+    }%?)`
+  );
+  console.log(
+    `Confidence ratio: ${confidenceRatioPct.toFixed(5)}% (< ${
+      priceConfig.confidenceRatio
+    }%?)`
+  );
+
+  const result =
+    timeDifference >= priceConfig.timeDifference ||
+    priceDeviationPct >= priceConfig.priceDeviation ||
+    confidenceRatioPct >= priceConfig.confidenceRatio;
+
+  if (result == true) {
+    console.log(
+      "Some of the above values passed the threshold. Will push the price."
+    );
+  } else {
+    console.log(
+      "None of the above values passed the threshold. No push needed."
+    );
+  }
+
+  return result;
+}

+ 0 - 226
price_pusher/src/pusher.ts

@@ -1,226 +0,0 @@
-import {
-  EvmPriceServiceConnection,
-  UnixTimestamp,
-} from "@pythnetwork/pyth-evm-js";
-import { addLeading0x, DurationInSeconds, sleep } from "./utils";
-import { PriceInfo, PriceListener } from "./price-listener";
-import { Contract } from "web3-eth-contract";
-import { PriceConfig } from "./price-config";
-import { TransactionReceipt } from "ethereum-protocol";
-import { PythContractFactory } from "./pyth-contract-factory";
-
-export class Pusher {
-  private connection: EvmPriceServiceConnection;
-  private pythContract: Contract;
-  private pythContractFactory: PythContractFactory;
-  private targetPriceListener: PriceListener;
-  private sourcePriceListener: PriceListener;
-  private priceConfigs: PriceConfig[];
-
-  private cooldownDuration: DurationInSeconds;
-
-  constructor(
-    connection: EvmPriceServiceConnection,
-    pythContractFactory: PythContractFactory,
-    targetPriceListener: PriceListener,
-    sourcePriceListener: PriceListener,
-    priceConfigs: PriceConfig[],
-    config: {
-      cooldownDuration: DurationInSeconds;
-    }
-  ) {
-    this.connection = connection;
-    this.targetPriceListener = targetPriceListener;
-    this.sourcePriceListener = sourcePriceListener;
-    this.priceConfigs = priceConfigs;
-
-    this.cooldownDuration = config.cooldownDuration;
-
-    this.pythContractFactory = pythContractFactory;
-    this.pythContract = this.pythContractFactory.createPythContractWithPayer();
-  }
-
-  async start() {
-    for (;;) {
-      const pricesToPush: PriceConfig[] = [];
-      const pubTimesToPush: UnixTimestamp[] = [];
-
-      for (const priceConfig of this.priceConfigs) {
-        const priceId = priceConfig.id;
-
-        const targetLatestPrice =
-          this.targetPriceListener.getLatestPriceInfo(priceId);
-        const sourceLatestPrice =
-          this.sourcePriceListener.getLatestPriceInfo(priceId);
-
-        if (
-          this.shouldUpdate(priceConfig, sourceLatestPrice, targetLatestPrice)
-        ) {
-          pricesToPush.push(priceConfig);
-          pubTimesToPush.push((targetLatestPrice?.publishTime || 0) + 1);
-        }
-      }
-      this.pushUpdates(pricesToPush, pubTimesToPush);
-      await sleep(this.cooldownDuration * 1000);
-    }
-  }
-
-  // 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)
-  // and will help multiple price pushers to have consistent behaviour.
-  async pushUpdates(
-    pricesToPush: PriceConfig[],
-    pubTimesToPush: UnixTimestamp[]
-  ) {
-    if (pricesToPush.length === 0) {
-      return;
-    }
-
-    const priceIds = pricesToPush.map((priceConfig) =>
-      addLeading0x(priceConfig.id)
-    );
-
-    const priceFeedUpdateData = await this.connection.getPriceFeedsUpdateData(
-      priceIds
-    );
-
-    console.log(
-      "Pushing ",
-      pricesToPush.map(
-        (priceConfig) => `${priceConfig.alias} (${priceConfig.id})`
-      )
-    );
-
-    const updateFee = await this.pythContract.methods
-      .getUpdateFee(priceFeedUpdateData)
-      .call();
-    console.log(`Update fee: ${updateFee}`);
-
-    this.pythContract.methods
-      .updatePriceFeedsIfNecessary(
-        priceFeedUpdateData,
-        priceIds,
-        pubTimesToPush
-      )
-      .send({ value: updateFee })
-      .on("transactionHash", (hash: string) => {
-        console.log(`Successful. Tx hash: ${hash}`);
-      })
-      .on("error", (err: Error, receipt: TransactionReceipt) => {
-        if (
-          err.message.includes(
-            "VM Exception while processing transaction: 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.
-          console.log(
-            "Execution reverted. With high probablity, the target chain price " +
-              "has already updated, Skipping this push."
-          );
-          return;
-        }
-
-        if (err.message.includes("the tx doesn't have the correct nonce.")) {
-          console.log(
-            "Multiple users are using the same accounts and nonce is incorrect. Skipping this push."
-          );
-          return;
-        }
-
-        if (
-          err.message.includes("sender doesn't have enough funds to send tx.")
-        ) {
-          console.error("Payer is out of balance, please top it up.");
-          throw err;
-        }
-
-        console.error("An unidentified error has occured:");
-        console.error(receipt);
-        throw err;
-      });
-  }
-
-  /**
-   * Checks whether on-chain price needs to be updated with the latest pyth price information.
-   *
-   * @param priceConfig Config of the price feed to check
-   * @returns True if the on-chain price needs to be updated.
-   */
-  shouldUpdate(
-    priceConfig: PriceConfig,
-    sourceLatestPrice: PriceInfo | undefined,
-    targetLatestPrice: PriceInfo | undefined
-  ): boolean {
-    const priceId = priceConfig.id;
-
-    // There is no price to update the target with.
-    if (sourceLatestPrice === undefined) {
-      return false;
-    }
-
-    // It means that price never existed there. So we should push the latest price feed.
-    if (targetLatestPrice === undefined) {
-      console.log(
-        `${priceConfig.alias} (${priceId}) is not available on the target network. Pushing the price.`
-      );
-      return true;
-    }
-
-    // The current price is not newer than the price onchain
-    if (sourceLatestPrice.publishTime < targetLatestPrice.publishTime) {
-      return false;
-    }
-
-    const timeDifference =
-      sourceLatestPrice.publishTime - targetLatestPrice.publishTime;
-
-    const priceDeviationPct =
-      (Math.abs(
-        Number(sourceLatestPrice.price) - Number(targetLatestPrice.price)
-      ) /
-        Number(targetLatestPrice.price)) *
-      100;
-    const confidenceRatioPct = Math.abs(
-      (Number(sourceLatestPrice.conf) / Number(sourceLatestPrice.price)) * 100
-    );
-
-    console.log(`Analyzing price ${priceConfig.alias} (${priceId})`);
-
-    console.log("Source latest price: ", sourceLatestPrice);
-    console.log("Target latest price: ", targetLatestPrice);
-
-    console.log(
-      `Time difference: ${timeDifference} (< ${priceConfig.timeDifference}?)`
-    );
-    console.log(
-      `Price deviation: ${priceDeviationPct.toFixed(5)}% (< ${
-        priceConfig.priceDeviation
-      }%?)`
-    );
-    console.log(
-      `Confidence ratio: ${confidenceRatioPct.toFixed(5)}% (< ${
-        priceConfig.confidenceRatio
-      }%?)`
-    );
-
-    const result =
-      timeDifference >= priceConfig.timeDifference ||
-      priceDeviationPct >= priceConfig.priceDeviation ||
-      confidenceRatioPct >= priceConfig.confidenceRatio;
-
-    if (result == true) {
-      console.log(
-        "Some of the above values passed the threshold. Will push the price."
-      );
-    } else {
-      console.log(
-        "None of the above values passed the threshold. No push needed."
-      );
-    }
-
-    return result;
-  }
-}

+ 0 - 82
price_pusher/src/pyth-contract-factory.ts

@@ -1,82 +0,0 @@
-import AbstractPythAbi from "@pythnetwork/pyth-sdk-solidity/abis/AbstractPyth.json";
-import HDWalletProvider from "@truffle/hdwallet-provider";
-import { Contract } from "web3-eth-contract";
-import { Provider } from "web3/providers";
-import Web3 from "web3";
-import { isWsEndpoint } from "./utils";
-
-export class PythContractFactory {
-  constructor(
-    private endpoint: string,
-    private mnemonic: string,
-    private pythContractAddr: 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 = new HDWalletProvider({
-      mnemonic: {
-        phrase: this.mnemonic,
-      },
-      providerOrUrl: this.createWeb3Provider() as Provider,
-    });
-
-    const web3 = new Web3(provider as any);
-
-    return new web3.eth.Contract(
-      AbstractPythAbi as any,
-      this.pythContractAddr,
-      {
-        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.pythContractAddr);
-  }
-
-  hasWebsocketProvider(): boolean {
-    return isWsEndpoint(this.endpoint);
-  }
-
-  private createWeb3Provider() {
-    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,
-      });
-    }
-  }
-}

+ 5 - 11
price_pusher/src/pyth-price-listener.ts

@@ -1,21 +1,15 @@
-import {
-  EvmPriceServiceConnection,
-  HexString,
-  PriceFeed,
-} from "@pythnetwork/pyth-evm-js";
+import { HexString, PriceFeed } from "@pythnetwork/pyth-evm-js";
+import { PriceServiceConnection } from "@pythnetwork/pyth-common-js";
 import { PriceConfig } from "./price-config";
-import { PriceInfo, PriceListener } from "./price-listener";
+import { PriceInfo, PriceListener } from "./interface";
 
 export class PythPriceListener implements PriceListener {
-  private connection: EvmPriceServiceConnection;
+  private connection: PriceServiceConnection;
   private priceIds: HexString[];
   private priceIdToAlias: Map<HexString, string>;
   private latestPriceInfo: Map<HexString, PriceInfo>;
 
-  constructor(
-    connection: EvmPriceServiceConnection,
-    priceConfigs: PriceConfig[]
-  ) {
+  constructor(connection: PriceServiceConnection, priceConfigs: PriceConfig[]) {
     this.connection = connection;
     this.priceIds = priceConfigs.map((priceConfig) => priceConfig.id);
     this.priceIdToAlias = new Map(