| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- import {
- HexString,
- PriceServiceConnection,
- } from "@pythnetwork/price-service-client";
- import {
- IPricePusher,
- PriceInfo,
- ChainPriceListener,
- PriceItem,
- } from "../interface";
- import { DurationInSeconds } from "../utils";
- import {
- ChainGrpcAuthApi,
- ChainGrpcWasmApi,
- MsgExecuteContract,
- Msgs,
- PrivateKey,
- TxGrpcClient,
- TxResponse,
- createTransactionFromMsg,
- } from "@injectivelabs/sdk-ts";
- import { Account } from "@injectivelabs/sdk-ts/dist/cjs/client/chain/types/auth";
- const DEFAULT_GAS_PRICE = 500000000;
- type PriceQueryResponse = {
- price_feed: {
- id: string;
- price: {
- price: string;
- conf: string;
- expo: number;
- publish_time: number;
- };
- };
- };
- type UpdateFeeResponse = {
- denom: string;
- amount: string;
- };
- // this use price without leading 0x
- export class InjectivePriceListener extends ChainPriceListener {
- constructor(
- private pythContractAddress: string,
- private grpcEndpoint: string,
- priceItems: PriceItem[],
- config: {
- pollingFrequency: DurationInSeconds;
- }
- ) {
- super("Injective", config.pollingFrequency, priceItems);
- }
- async getOnChainPriceInfo(
- priceId: HexString
- ): Promise<PriceInfo | undefined> {
- let priceQueryResponse: PriceQueryResponse;
- try {
- const api = new ChainGrpcWasmApi(this.grpcEndpoint);
- const { data } = await api.fetchSmartContractState(
- this.pythContractAddress,
- Buffer.from(`{"price_feed":{"id":"${priceId}"}}`).toString("base64")
- );
- const json = Buffer.from(data).toString();
- priceQueryResponse = JSON.parse(json);
- } catch (e) {
- console.error(`Polling on-chain price for ${priceId} failed. Error:`);
- console.error(e);
- return undefined;
- }
- console.log(
- `Polled an Injective on chain price for feed ${this.priceIdToAlias.get(
- priceId
- )} (${priceId}).`
- );
- return {
- conf: priceQueryResponse.price_feed.price.conf,
- price: priceQueryResponse.price_feed.price.price,
- publishTime: priceQueryResponse.price_feed.price.publish_time,
- };
- }
- }
- type InjectiveConfig = {
- chainId: string;
- gasMultiplier: number;
- gasPrice: number;
- };
- export class InjectivePricePusher implements IPricePusher {
- private wallet: PrivateKey;
- private chainConfig: InjectiveConfig;
- private account: Account | null = null;
- constructor(
- private priceServiceConnection: PriceServiceConnection,
- private pythContractAddress: string,
- private grpcEndpoint: string,
- mnemonic: string,
- chainConfig?: Partial<InjectiveConfig>
- ) {
- this.wallet = PrivateKey.fromMnemonic(mnemonic);
- this.chainConfig = {
- chainId: chainConfig?.chainId ?? "injective-888",
- gasMultiplier: chainConfig?.gasMultiplier ?? 1.2,
- gasPrice: chainConfig?.gasPrice ?? DEFAULT_GAS_PRICE,
- };
- }
- private injectiveAddress(): string {
- return this.wallet.toBech32();
- }
- private async signAndBroadcastMsg(msg: Msgs): Promise<TxResponse> {
- const chainGrpcAuthApi = new ChainGrpcAuthApi(this.grpcEndpoint);
- // Fetch the latest account details only if it's not stored.
- this.account ??= await chainGrpcAuthApi.fetchAccount(
- this.injectiveAddress()
- );
- const { txRaw: simulateTxRaw } = createTransactionFromMsg({
- sequence: this.account.baseAccount.sequence,
- accountNumber: this.account.baseAccount.accountNumber,
- message: msg,
- chainId: this.chainConfig.chainId,
- pubKey: this.wallet.toPublicKey().toBase64(),
- });
- const txService = new TxGrpcClient(this.grpcEndpoint);
- // simulation
- try {
- const {
- gasInfo: { gasUsed },
- } = await txService.simulate(simulateTxRaw);
- // simulation returns us the approximate gas used
- // gas passed with the transaction should be more than that
- // in order for it to be successfully executed
- // this multiplier takes care of that
- const gas = (gasUsed * this.chainConfig.gasMultiplier).toFixed();
- const fee = {
- amount: [
- {
- denom: "inj",
- amount: (Number(gas) * this.chainConfig.gasPrice).toFixed(),
- },
- ],
- gas,
- };
- const { signBytes, txRaw } = createTransactionFromMsg({
- sequence: this.account.baseAccount.sequence,
- accountNumber: this.account.baseAccount.accountNumber,
- message: msg,
- chainId: this.chainConfig.chainId,
- fee,
- pubKey: this.wallet.toPublicKey().toBase64(),
- });
- const sig = await this.wallet.sign(Buffer.from(signBytes));
- this.account.baseAccount.sequence++;
- /** Append Signatures */
- txRaw.signatures = [sig];
- // this takes approx 5 seconds
- const txResponse = await txService.broadcast(txRaw);
- return txResponse;
- } catch (e: any) {
- // The sequence number was invalid and hence we will have to fetch it again.
- if (JSON.stringify(e).match(/account sequence mismatch/) !== null) {
- // We need to fetch the account details again.
- this.account = null;
- }
- throw e;
- }
- }
- async getPriceFeedUpdateObject(priceIds: string[]): Promise<any> {
- const vaas = await this.priceServiceConnection.getLatestVaas(priceIds);
- return {
- update_price_feeds: {
- data: vaas,
- },
- };
- }
- async updatePriceFeed(
- priceIds: string[],
- pubTimesToPush: number[]
- ): Promise<void> {
- if (priceIds.length === 0) {
- return;
- }
- if (priceIds.length !== pubTimesToPush.length)
- throw new Error("Invalid arguments");
- let priceFeedUpdateObject;
- try {
- // get the latest VAAs for updatePriceFeed and then push them
- priceFeedUpdateObject = await this.getPriceFeedUpdateObject(priceIds);
- } catch (e) {
- console.error("Error fetching the latest vaas to push");
- console.error(e);
- return;
- }
- let updateFeeQueryResponse: UpdateFeeResponse;
- try {
- const api = new ChainGrpcWasmApi(this.grpcEndpoint);
- const { data } = await api.fetchSmartContractState(
- this.pythContractAddress,
- Buffer.from(
- JSON.stringify({
- get_update_fee: {
- vaas: priceFeedUpdateObject.update_price_feeds.data,
- },
- })
- ).toString("base64")
- );
- const json = Buffer.from(data).toString();
- updateFeeQueryResponse = JSON.parse(json);
- } catch (e) {
- console.error("Error fetching update fee");
- console.error(e);
- return;
- }
- try {
- const executeMsg = MsgExecuteContract.fromJSON({
- sender: this.injectiveAddress(),
- contractAddress: this.pythContractAddress,
- msg: priceFeedUpdateObject,
- funds: [updateFeeQueryResponse],
- });
- const rs = await this.signAndBroadcastMsg(executeMsg);
- console.log("Succesfully broadcasted txHash:", rs.txHash);
- } catch (e: any) {
- if (e.message.match(/account inj[a-zA-Z0-9]+ not found/) !== null) {
- console.error(e);
- throw new Error("Please check the mnemonic");
- }
- if (
- e.message.match(/insufficient/) !== null &&
- e.message.match(/funds/) !== null
- ) {
- console.error(e);
- throw new Error("Insufficient funds");
- }
- console.error("Error executing messages");
- console.log(e);
- }
- }
- }
|