Ver código fonte

feat(price_pusher): add price_pusher for ton (#2127)

* add price_pusher for ton

* add ton command

* fix

* address comments

* bump

* fix
Daniel Chew 1 ano atrás
pai
commit
51bf54204f

+ 7 - 0
apps/price_pusher/config.ton.mainnet.sample.json

@@ -0,0 +1,7 @@
+{
+  "endpoint": "https://toncenter.com/api/v2/jsonRPC",
+  "pyth-contract-address": "EQBU6k8HH6yX4Jf3d18swWbnYr31D3PJI7PgjXT",
+  "price-service-endpoint": "https://hermes.pyth.network",
+  "private-key-file": "./mnemonic",
+  "price-config-file": "./price-config.stable.sample.yaml"
+}

+ 7 - 0
apps/price_pusher/config.ton.testnet.sample.json

@@ -0,0 +1,7 @@
+{
+  "endpoint": "https://testnet.toncenter.com/api/v2/jsonRPC",
+  "pyth-contract-address": "EQB4ZnrI5qsP_IUJgVJNwEGKLzZWsQOFhiaqDbD7pTt_f9oU",
+  "price-service-endpoint": "https://hermes.pyth.network",
+  "private-key-file": "./mnemonic",
+  "price-config-file": "./price-config.stable.sample.yaml"
+}

+ 5 - 2
apps/price_pusher/package.json

@@ -1,6 +1,6 @@
 {
   "name": "@pythnetwork/price-pusher",
-  "version": "8.3.0",
+  "version": "8.3.1",
   "description": "Pyth Price Pusher",
   "homepage": "https://pyth.network",
   "main": "lib/index.js",
@@ -62,12 +62,15 @@
     "@mysten/sui": "^1.3.0",
     "@pythnetwork/price-service-client": "workspace:*",
     "@pythnetwork/price-service-sdk": "workspace:^",
+    "@pythnetwork/pyth-fuel-js": "workspace:*",
     "@pythnetwork/pyth-sdk-solidity": "workspace:*",
     "@pythnetwork/pyth-solana-receiver": "workspace:*",
     "@pythnetwork/pyth-sui-js": "workspace:*",
+    "@pythnetwork/pyth-ton-js": "workspace:*",
     "@pythnetwork/solana-utils": "workspace:*",
-    "@pythnetwork/pyth-fuel-js": "workspace:*",
     "@solana/web3.js": "^1.93.0",
+    "@ton/crypto": "^3.3.0",
+    "@ton/ton": "^15.1.0",
     "@types/pino": "^7.0.5",
     "aptos": "^1.8.5",
     "fuels": "^0.94.5",

+ 2 - 0
apps/price_pusher/src/index.ts

@@ -8,6 +8,7 @@ import sui from "./sui/command";
 import near from "./near/command";
 import solana from "./solana/command";
 import fuel from "./fuel/command";
+import ton from "./ton/command";
 
 yargs(hideBin(process.argv))
   .parserConfiguration({
@@ -22,4 +23,5 @@ yargs(hideBin(process.argv))
   .command(sui)
   .command(near)
   .command(solana)
+  .command(ton)
   .help().argv;

+ 107 - 0
apps/price_pusher/src/ton/command.ts

@@ -0,0 +1,107 @@
+import { Options } from "yargs";
+import * as options from "../options";
+import { readPriceConfigFile } from "../price-config";
+import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import { PythPriceListener } from "../pyth-price-listener";
+import { TonPriceListener, TonPricePusher } from "./ton";
+import { Controller } from "../controller";
+import { Address, TonClient } from "@ton/ton";
+import fs from "fs";
+import pino from "pino";
+
+export default {
+  command: "ton",
+  describe: "run price pusher for TON",
+  builder: {
+    endpoint: {
+      description: "TON RPC API endpoint",
+      type: "string",
+      required: true,
+    } as Options,
+    "private-key-file": {
+      description: "Path to the private key file",
+      type: "string",
+      required: true,
+    } as Options,
+    "pyth-contract-address": {
+      description: "Pyth contract address on TON",
+      type: "string",
+      required: true,
+    } as Options,
+    ...options.priceConfigFile,
+    ...options.priceServiceEndpoint,
+    ...options.pushingFrequency,
+    ...options.pollingFrequency,
+    ...options.logLevel,
+    ...options.priceServiceConnectionLogLevel,
+    ...options.controllerLogLevel,
+  },
+  handler: async function (argv: any) {
+    const {
+      endpoint,
+      privateKeyFile,
+      pythContractAddress,
+      priceConfigFile,
+      priceServiceEndpoint,
+      pushingFrequency,
+      pollingFrequency,
+      logLevel,
+      priceServiceConnectionLogLevel,
+      controllerLogLevel,
+    } = argv;
+
+    const logger = pino({ level: logLevel });
+
+    const priceConfigs = readPriceConfigFile(priceConfigFile);
+
+    const priceServiceConnection = new PriceServiceConnection(
+      priceServiceEndpoint,
+      {
+        logger: logger.child(
+          { module: "PriceServiceConnection" },
+          { level: priceServiceConnectionLogLevel }
+        ),
+      }
+    );
+
+    const priceItems = priceConfigs.map(({ id, alias }) => ({ id, alias }));
+
+    const pythListener = new PythPriceListener(
+      priceServiceConnection,
+      priceItems,
+      logger.child({ module: "PythPriceListener" })
+    );
+
+    const client = new TonClient({ endpoint });
+    const privateKey = fs.readFileSync(privateKeyFile, "utf8").trim();
+    const contractAddress = Address.parse(pythContractAddress);
+    const provider = client.provider(contractAddress);
+
+    const tonPriceListener = new TonPriceListener(
+      provider,
+      contractAddress,
+      priceItems,
+      logger.child({ module: "TonPriceListener" }),
+      { pollingFrequency }
+    );
+
+    const tonPricePusher = new TonPricePusher(
+      client,
+      privateKey,
+      contractAddress,
+      priceServiceConnection,
+      logger.child({ module: "TonPricePusher" })
+    );
+
+    const controller = new Controller(
+      priceConfigs,
+      pythListener,
+      tonPriceListener,
+      tonPricePusher,
+      logger.child({ module: "Controller" }, { level: controllerLogLevel }),
+      { pushingFrequency }
+    );
+
+    await controller.start();
+  },
+};

+ 127 - 0
apps/price_pusher/src/ton/ton.ts

@@ -0,0 +1,127 @@
+import { PriceServiceConnection } from "@pythnetwork/price-service-client";
+import {
+  ChainPriceListener,
+  IPricePusher,
+  PriceInfo,
+  PriceItem,
+} from "../interface";
+import { addLeading0x, DurationInSeconds } from "../utils";
+import { Logger } from "pino";
+import {
+  Address,
+  ContractProvider,
+  OpenedContract,
+  Sender,
+  TonClient,
+  WalletContractV4,
+} from "@ton/ton";
+import { keyPairFromSeed } from "@ton/crypto";
+import {
+  PythContract,
+  calculateUpdatePriceFeedsFee,
+} from "@pythnetwork/pyth-ton-js";
+
+export class TonPriceListener extends ChainPriceListener {
+  private contract: OpenedContract<PythContract>;
+
+  constructor(
+    private provider: ContractProvider,
+    private contractAddress: Address,
+    priceItems: PriceItem[],
+    private logger: Logger,
+    config: {
+      pollingFrequency: DurationInSeconds;
+    }
+  ) {
+    super(config.pollingFrequency, priceItems);
+    this.contract = this.provider.open(
+      PythContract.createFromAddress(this.contractAddress)
+    );
+  }
+
+  async getOnChainPriceInfo(priceId: string): Promise<PriceInfo | undefined> {
+    try {
+      const formattedPriceId = addLeading0x(priceId);
+      const priceInfo = await this.contract.getPriceUnsafe(formattedPriceId);
+
+      this.logger.debug(
+        `Polled a TON on chain price for feed ${this.priceIdToAlias.get(
+          priceId
+        )} (${priceId}).`
+      );
+
+      return {
+        conf: priceInfo.conf.toString(),
+        price: priceInfo.price.toString(),
+        publishTime: priceInfo.publishTime,
+      };
+    } catch (err) {
+      this.logger.error({ err, priceId }, `Polling on-chain price failed.`);
+      return undefined;
+    }
+  }
+}
+
+export class TonPricePusher implements IPricePusher {
+  private contract: OpenedContract<PythContract>;
+  private sender: Sender;
+
+  constructor(
+    private client: TonClient,
+    private privateKey: string,
+    private contractAddress: Address,
+    private priceServiceConnection: PriceServiceConnection,
+    private logger: Logger
+  ) {
+    this.contract = this.client
+      .provider(this.contractAddress)
+      .open(PythContract.createFromAddress(this.contractAddress));
+    const keyPair = keyPairFromSeed(Buffer.from(this.privateKey, "hex"));
+    const wallet = WalletContractV4.create({
+      publicKey: keyPair.publicKey,
+      workchain: 0, // workchain 0 is the masterchain
+    });
+    const provider = this.client.open(wallet);
+    this.sender = provider.sender(keyPair.secretKey);
+  }
+
+  async updatePriceFeed(
+    priceIds: string[],
+    // eslint-disable-next-line @typescript-eslint/no-unused-vars
+    pubTimesToPush: number[]
+  ): Promise<void> {
+    if (priceIds.length === 0) {
+      return;
+    }
+
+    let priceFeedUpdateData: string[];
+    try {
+      priceFeedUpdateData = await this.priceServiceConnection.getLatestVaas(
+        priceIds
+      );
+    } catch (err: any) {
+      this.logger.error(err, "getPriceFeedsUpdateData failed");
+      return;
+    }
+
+    try {
+      for (const updateData of priceFeedUpdateData) {
+        const updateDataBuffer = Buffer.from(updateData, "base64");
+        const updateFee = await this.contract.getUpdateFee(updateDataBuffer);
+        const totalFee =
+          calculateUpdatePriceFeedsFee(BigInt(priceIds.length)) +
+          BigInt(updateFee);
+
+        await this.contract.sendUpdatePriceFeeds(
+          this.sender,
+          updateDataBuffer,
+          totalFee
+        );
+      }
+
+      this.logger.info("updatePriceFeed successful");
+    } catch (err: any) {
+      this.logger.error(err, "updatePriceFeed failed");
+    }
+  }
+}

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


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