Parcourir la source

[price_pusher] Add near command (#1306)

* add near command

* add try catch to getPriceFeedsUpdateData and getUpdateFeeEstimate

* add private-key-path optional parameter

* chore: run pre-commit

* fix: make private key optional

* chore: bump version

---------

Co-authored-by: Ali Behjati <bahjatia@gmail.com>
MagicGordon il y a 1 an
Parent
commit
c8acfc5660

Fichier diff supprimé car celui-ci est trop grand
+ 886 - 1
package-lock.json


+ 12 - 0
price_pusher/README.md

@@ -133,6 +133,18 @@ npm run start -- sui \
   [--polling-frequency 5] \
   [--num-gas-objects 30]
 
+# For Near
+npm run start -- near \
+  --node-url https://rpc.testnet.near.org \
+  --network testnet \
+  --account-id payer.testnet \
+  --pyth-contract-address pyth-oracle.testnet \
+  --price-service-endpoint "https://hermes-beta.pyth.network" \
+  --price-config-file ./price-config.beta.sample.yaml \
+  [--private-key-path ./payer.testnet.json] \
+  [--pushing-frequency 10] \
+  [--polling-frequency 5]
+
 
 # Or, run the price pusher docker image instead of building from the source
 docker run public.ecr.aws/pyth-network/xc-price-pusher:v<version> -- <above-arguments>

+ 8 - 0
price_pusher/config.near.mainnet.sample.json

@@ -0,0 +1,8 @@
+{
+  "node-url": "https://rpc.mainnet.near.org",
+  "network": "mainnet",
+  "account-id": "payer.near",
+  "pyth-contract-address": "pyth-oracle.near",
+  "price-service-endpoint": "https://hermes.pyth.network",
+  "price-config-file": "./price-config.stable.sample.yaml"
+}

+ 8 - 0
price_pusher/config.near.testnet.sample.json

@@ -0,0 +1,8 @@
+{
+  "node-url": "https://rpc.testnet.near.org",
+  "network": "testnet",
+  "account-id": "payer.testnet",
+  "pyth-contract-address": "pyth-oracle.testnet",
+  "price-service-endpoint": "https://hermes-beta.pyth.network",
+  "price-config-file": "./price-config.beta.sample.yaml"
+}

+ 2 - 1
price_pusher/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/price-pusher",
-  "version": "6.1.0",
+  "version": "6.2.0",
   "description": "Pyth Price Pusher",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",
@@ -59,6 +59,7 @@
     "@truffle/hdwallet-provider": "^2.1.3",
     "aptos": "^1.8.5",
     "joi": "^17.6.0",
+    "near-api-js": "^3.0.2",
     "web3": "^1.8.1",
     "web3-eth-contract": "^1.8.1",
     "yaml": "^2.1.1",

+ 2 - 0
price_pusher/src/index.ts

@@ -5,6 +5,7 @@ import injective from "./injective/command";
 import evm from "./evm/command";
 import aptos from "./aptos/command";
 import sui from "./sui/command";
+import near from "./near/command";
 
 yargs(hideBin(process.argv))
   .config("config")
@@ -13,4 +14,5 @@ yargs(hideBin(process.argv))
   .command(injective)
   .command(aptos)
   .command(sui)
+  .command(near)
   .help().argv;

+ 100 - 0
price_pusher/src/near/command.ts

@@ -0,0 +1,100 @@
+import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import * as options from "../options";
+import { readPriceConfigFile } from "../price-config";
+import { PythPriceListener } from "../pyth-price-listener";
+import { Controller } from "../controller";
+import { Options } from "yargs";
+import { NearAccount, NearPriceListener, NearPricePusher } from "./near";
+
+export default {
+  command: "near",
+  describe: "run price pusher for near",
+  builder: {
+    "node-url": {
+      description:
+        "NEAR RPC API url. used to make JSON RPC calls to interact with NEAR.",
+      type: "string",
+      required: true,
+    } as Options,
+    network: {
+      description: "testnet or mainnet.",
+      type: "string",
+      required: true,
+    } as Options,
+    "account-id": {
+      description: "payer account identifier.",
+      type: "string",
+      required: true,
+    } as Options,
+    "private-key-path": {
+      description: "path to payer private key file.",
+      type: "string",
+      required: false,
+    } as Options,
+    ...options.priceConfigFile,
+    ...options.priceServiceEndpoint,
+    ...options.pythContractAddress,
+    ...options.pollingFrequency,
+    ...options.pushingFrequency,
+  },
+  handler: function (argv: any) {
+    // FIXME: type checks for this
+    const {
+      nodeUrl,
+      network,
+      accountId,
+      privateKeyPath,
+      priceConfigFile,
+      priceServiceEndpoint,
+      pythContractAddress,
+      pushingFrequency,
+      pollingFrequency,
+    } = argv;
+
+    const priceConfigs = readPriceConfigFile(priceConfigFile);
+    const priceServiceConnection = new PriceServiceConnection(
+      priceServiceEndpoint,
+      {
+        logger: {
+          // Log only warnings and errors from the price service client
+          info: () => undefined,
+          warn: console.warn,
+          error: console.error,
+          debug: () => undefined,
+          trace: () => undefined,
+        },
+      }
+    );
+
+    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    const pythListener = new PythPriceListener(
+      priceServiceConnection,
+      priceItems
+    );
+
+    const nearAccount = new NearAccount(
+      network,
+      accountId,
+      nodeUrl,
+      privateKeyPath,
+      pythContractAddress
+    );
+
+    const nearListener = new NearPriceListener(nearAccount, priceItems, {
+      pollingFrequency,
+    });
+
+    const nearPusher = new NearPricePusher(nearAccount, priceServiceConnection);
+
+    const controller = new Controller(
+      priceConfigs,
+      pythListener,
+      nearListener,
+      nearPusher,
+      { pushingFrequency }
+    );
+
+    controller.start();
+  },
+};

+ 233 - 0
price_pusher/src/near/near.ts

@@ -0,0 +1,233 @@
+import os from "os";
+import path from "path";
+import fs from "fs";
+
+import {
+  IPricePusher,
+  PriceInfo,
+  ChainPriceListener,
+  PriceItem,
+} from "../interface";
+import {
+  PriceServiceConnection,
+  HexString,
+} from "@pythnetwork/price-service-client";
+import { DurationInSeconds } from "../utils";
+
+import { Account, Connection, KeyPair } from "near-api-js";
+import {
+  ExecutionStatus,
+  ExecutionStatusBasic,
+  FinalExecutionOutcome,
+} from "near-api-js/lib/providers/provider";
+import { InMemoryKeyStore } from "near-api-js/lib/key_stores";
+
+export class NearPriceListener extends ChainPriceListener {
+  constructor(
+    private account: NearAccount,
+    priceItems: PriceItem[],
+    config: {
+      pollingFrequency: DurationInSeconds;
+    }
+  ) {
+    super("near", config.pollingFrequency, priceItems);
+  }
+
+  async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
+    try {
+      const priceRaw = await this.account.getPriceUnsafe(priceId);
+
+      console.log(
+        `Polled a NEAR on chain price for feed ${this.priceIdToAlias.get(
+          priceId
+        )} (${priceId}) ${JSON.stringify(priceRaw)}.`
+      );
+
+      if (priceRaw) {
+        return {
+          conf: priceRaw.conf,
+          price: priceRaw.price,
+          publishTime: priceRaw.publish_time,
+        };
+      } else {
+        return undefined;
+      }
+    } catch (e) {
+      console.error(`Polling on-chain price for ${priceId} failed. Error:`);
+      console.error(e);
+      return undefined;
+    }
+  }
+}
+
+export class NearPricePusher implements IPricePusher {
+  constructor(
+    private account: NearAccount,
+    private connection: PriceServiceConnection
+  ) {}
+
+  async updatePriceFeed(
+    priceIds: string[],
+    pubTimesToPush: number[]
+  ): Promise<void> {
+    if (priceIds.length === 0) {
+      return;
+    }
+
+    if (priceIds.length !== pubTimesToPush.length)
+      throw new Error("Invalid arguments");
+
+    let priceFeedUpdateData;
+    try {
+      priceFeedUpdateData = await this.getPriceFeedsUpdateData(priceIds);
+    } catch (e: any) {
+      console.error(new Date(), "getPriceFeedsUpdateData failed:", e);
+      return;
+    }
+
+    console.log("Pushing ", priceIds);
+
+    for (const data of priceFeedUpdateData) {
+      let updateFee;
+      try {
+        updateFee = await this.account.getUpdateFeeEstimate(data);
+        console.log(`Update fee: ${updateFee}`);
+      } catch (e: any) {
+        console.error(new Date(), "getUpdateFeeEstimate failed:", e);
+        continue;
+      }
+
+      try {
+        const outcome = await this.account.updatePriceFeeds(data, updateFee);
+        const failureMessages: (ExecutionStatus | ExecutionStatusBasic)[] = [];
+        const is_success = Object.values(outcome["receipts_outcome"]).reduce(
+          (is_success, receipt) => {
+            if (
+              Object.prototype.hasOwnProperty.call(
+                receipt["outcome"]["status"],
+                "Failure"
+              )
+            ) {
+              failureMessages.push(receipt["outcome"]["status"]);
+              return false;
+            }
+            return is_success;
+          },
+          true
+        );
+        if (is_success) {
+          console.log(
+            new Date(),
+            "updatePriceFeeds successful. Tx hash: ",
+            outcome["transaction"]["hash"]
+          );
+        } else {
+          console.error(
+            new Date(),
+            "updatePriceFeeds failed:",
+            JSON.stringify(failureMessages, undefined, 2)
+          );
+        }
+      } catch (e: any) {
+        console.error(new Date(), "updatePriceFeeds failed:", e);
+      }
+    }
+  }
+
+  private async getPriceFeedsUpdateData(
+    priceIds: HexString[]
+  ): Promise<string[]> {
+    const latestVaas = await this.connection.getLatestVaas(priceIds);
+    return latestVaas.map((vaa) => Buffer.from(vaa, "base64").toString("hex"));
+  }
+}
+
+export class NearAccount {
+  private account: Account;
+
+  constructor(
+    network: string,
+    accountId: string,
+    nodeUrl: string,
+    privateKeyPath: string | undefined,
+    private pythAccountId: string
+  ) {
+    const connection = this.getConnection(
+      network,
+      accountId,
+      nodeUrl,
+      privateKeyPath
+    );
+    this.account = new Account(connection, accountId);
+  }
+
+  async getPriceUnsafe(priceId: string): Promise<any> {
+    return await this.account.viewFunction({
+      contractId: this.pythAccountId,
+      methodName: "get_price_unsafe",
+      args: {
+        price_identifier: priceId,
+      },
+    });
+  }
+
+  async getUpdateFeeEstimate(data: string): Promise<any> {
+    return await this.account.viewFunction({
+      contractId: this.pythAccountId,
+      methodName: "get_update_fee_estimate",
+      args: {
+        data,
+      },
+    });
+  }
+
+  async updatePriceFeeds(
+    data: string,
+    updateFee: any
+  ): Promise<FinalExecutionOutcome> {
+    return await this.account.functionCall({
+      contractId: this.pythAccountId,
+      methodName: "update_price_feeds",
+      args: {
+        data,
+      },
+      gas: "300000000000000" as any,
+      attachedDeposit: updateFee,
+    });
+  }
+
+  private getConnection(
+    network: string,
+    accountId: string,
+    nodeUrl: string,
+    privateKeyPath: string | undefined
+  ): Connection {
+    const content = fs.readFileSync(
+      privateKeyPath ||
+        path.join(
+          os.homedir(),
+          ".near-credentials",
+          network,
+          accountId + ".json"
+        )
+    );
+    const accountInfo = JSON.parse(content.toString());
+    let privateKey = accountInfo.private_key;
+    if (!privateKey && accountInfo.secret_key) {
+      privateKey = accountInfo.secret_key;
+    }
+    if (accountInfo.account_id && privateKey) {
+      const keyPair = KeyPair.fromString(privateKey);
+      const keyStore = new InMemoryKeyStore();
+      keyStore.setKey(network, accountInfo.account_id, keyPair);
+      return Connection.fromConfig({
+        networkId: network,
+        provider: { type: "JsonRpcProvider", args: { url: nodeUrl } },
+        signer: { type: "InMemorySigner", keyStore },
+        jsvmAccountId: `jsvm.${network}`,
+      });
+    } else {
+      throw new Error("Invalid key file!");
+    }
+  }
+}

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff