injective.ts 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. import {
  2. HexString,
  3. PriceServiceConnection,
  4. } from "@pythnetwork/price-service-client";
  5. import {
  6. IPricePusher,
  7. PriceInfo,
  8. ChainPriceListener,
  9. PriceItem,
  10. } from "../interface";
  11. import { DurationInSeconds } from "../utils";
  12. import {
  13. ChainGrpcAuthApi,
  14. ChainGrpcWasmApi,
  15. MsgExecuteContract,
  16. Msgs,
  17. PrivateKey,
  18. TxGrpcClient,
  19. TxResponse,
  20. createTransactionFromMsg,
  21. } from "@injectivelabs/sdk-ts";
  22. import { Account } from "@injectivelabs/sdk-ts/dist/cjs/client/chain/types/auth";
  23. const DEFAULT_GAS_PRICE = 500000000;
  24. type PriceQueryResponse = {
  25. price_feed: {
  26. id: string;
  27. price: {
  28. price: string;
  29. conf: string;
  30. expo: number;
  31. publish_time: number;
  32. };
  33. };
  34. };
  35. type UpdateFeeResponse = {
  36. denom: string;
  37. amount: string;
  38. };
  39. // this use price without leading 0x
  40. export class InjectivePriceListener extends ChainPriceListener {
  41. constructor(
  42. private pythContractAddress: string,
  43. private grpcEndpoint: string,
  44. priceItems: PriceItem[],
  45. config: {
  46. pollingFrequency: DurationInSeconds;
  47. }
  48. ) {
  49. super("Injective", config.pollingFrequency, priceItems);
  50. }
  51. async getOnChainPriceInfo(
  52. priceId: HexString
  53. ): Promise<PriceInfo | undefined> {
  54. let priceQueryResponse: PriceQueryResponse;
  55. try {
  56. const api = new ChainGrpcWasmApi(this.grpcEndpoint);
  57. const { data } = await api.fetchSmartContractState(
  58. this.pythContractAddress,
  59. Buffer.from(`{"price_feed":{"id":"${priceId}"}}`).toString("base64")
  60. );
  61. const json = Buffer.from(data).toString();
  62. priceQueryResponse = JSON.parse(json);
  63. } catch (e) {
  64. console.error(`Polling on-chain price for ${priceId} failed. Error:`);
  65. console.error(e);
  66. return undefined;
  67. }
  68. console.log(
  69. `Polled an Injective on chain price for feed ${this.priceIdToAlias.get(
  70. priceId
  71. )} (${priceId}).`
  72. );
  73. return {
  74. conf: priceQueryResponse.price_feed.price.conf,
  75. price: priceQueryResponse.price_feed.price.price,
  76. publishTime: priceQueryResponse.price_feed.price.publish_time,
  77. };
  78. }
  79. }
  80. type InjectiveConfig = {
  81. chainId: string;
  82. gasMultiplier: number;
  83. gasPrice: number;
  84. };
  85. export class InjectivePricePusher implements IPricePusher {
  86. private wallet: PrivateKey;
  87. private chainConfig: InjectiveConfig;
  88. private account: Account | null = null;
  89. constructor(
  90. private priceServiceConnection: PriceServiceConnection,
  91. private pythContractAddress: string,
  92. private grpcEndpoint: string,
  93. mnemonic: string,
  94. chainConfig?: Partial<InjectiveConfig>
  95. ) {
  96. this.wallet = PrivateKey.fromMnemonic(mnemonic);
  97. this.chainConfig = {
  98. chainId: chainConfig?.chainId ?? "injective-888",
  99. gasMultiplier: chainConfig?.gasMultiplier ?? 1.2,
  100. gasPrice: chainConfig?.gasPrice ?? DEFAULT_GAS_PRICE,
  101. };
  102. }
  103. private injectiveAddress(): string {
  104. return this.wallet.toBech32();
  105. }
  106. private async signAndBroadcastMsg(msg: Msgs): Promise<TxResponse> {
  107. const chainGrpcAuthApi = new ChainGrpcAuthApi(this.grpcEndpoint);
  108. // Fetch the latest account details only if it's not stored.
  109. this.account ??= await chainGrpcAuthApi.fetchAccount(
  110. this.injectiveAddress()
  111. );
  112. const { txRaw: simulateTxRaw } = createTransactionFromMsg({
  113. sequence: this.account.baseAccount.sequence,
  114. accountNumber: this.account.baseAccount.accountNumber,
  115. message: msg,
  116. chainId: this.chainConfig.chainId,
  117. pubKey: this.wallet.toPublicKey().toBase64(),
  118. });
  119. const txService = new TxGrpcClient(this.grpcEndpoint);
  120. // simulation
  121. try {
  122. const {
  123. gasInfo: { gasUsed },
  124. } = await txService.simulate(simulateTxRaw);
  125. // simulation returns us the approximate gas used
  126. // gas passed with the transaction should be more than that
  127. // in order for it to be successfully executed
  128. // this multiplier takes care of that
  129. const gas = (gasUsed * this.chainConfig.gasMultiplier).toFixed();
  130. const fee = {
  131. amount: [
  132. {
  133. denom: "inj",
  134. amount: (Number(gas) * this.chainConfig.gasPrice).toFixed(),
  135. },
  136. ],
  137. gas,
  138. };
  139. const { signBytes, txRaw } = createTransactionFromMsg({
  140. sequence: this.account.baseAccount.sequence,
  141. accountNumber: this.account.baseAccount.accountNumber,
  142. message: msg,
  143. chainId: this.chainConfig.chainId,
  144. fee,
  145. pubKey: this.wallet.toPublicKey().toBase64(),
  146. });
  147. const sig = await this.wallet.sign(Buffer.from(signBytes));
  148. this.account.baseAccount.sequence++;
  149. /** Append Signatures */
  150. txRaw.signatures = [sig];
  151. // this takes approx 5 seconds
  152. const txResponse = await txService.broadcast(txRaw);
  153. return txResponse;
  154. } catch (e: any) {
  155. // The sequence number was invalid and hence we will have to fetch it again.
  156. if (JSON.stringify(e).match(/account sequence mismatch/) !== null) {
  157. // We need to fetch the account details again.
  158. this.account = null;
  159. }
  160. throw e;
  161. }
  162. }
  163. async getPriceFeedUpdateObject(priceIds: string[]): Promise<any> {
  164. const vaas = await this.priceServiceConnection.getLatestVaas(priceIds);
  165. return {
  166. update_price_feeds: {
  167. data: vaas,
  168. },
  169. };
  170. }
  171. async updatePriceFeed(
  172. priceIds: string[],
  173. pubTimesToPush: number[]
  174. ): Promise<void> {
  175. if (priceIds.length === 0) {
  176. return;
  177. }
  178. if (priceIds.length !== pubTimesToPush.length)
  179. throw new Error("Invalid arguments");
  180. let priceFeedUpdateObject;
  181. try {
  182. // get the latest VAAs for updatePriceFeed and then push them
  183. priceFeedUpdateObject = await this.getPriceFeedUpdateObject(priceIds);
  184. } catch (e) {
  185. console.error("Error fetching the latest vaas to push");
  186. console.error(e);
  187. return;
  188. }
  189. let updateFeeQueryResponse: UpdateFeeResponse;
  190. try {
  191. const api = new ChainGrpcWasmApi(this.grpcEndpoint);
  192. const { data } = await api.fetchSmartContractState(
  193. this.pythContractAddress,
  194. Buffer.from(
  195. JSON.stringify({
  196. get_update_fee: {
  197. vaas: priceFeedUpdateObject.update_price_feeds.data,
  198. },
  199. })
  200. ).toString("base64")
  201. );
  202. const json = Buffer.from(data).toString();
  203. updateFeeQueryResponse = JSON.parse(json);
  204. } catch (e) {
  205. console.error("Error fetching update fee");
  206. console.error(e);
  207. return;
  208. }
  209. try {
  210. const executeMsg = MsgExecuteContract.fromJSON({
  211. sender: this.injectiveAddress(),
  212. contractAddress: this.pythContractAddress,
  213. msg: priceFeedUpdateObject,
  214. funds: [updateFeeQueryResponse],
  215. });
  216. const rs = await this.signAndBroadcastMsg(executeMsg);
  217. console.log("Succesfully broadcasted txHash:", rs.txHash);
  218. } catch (e: any) {
  219. if (e.message.match(/account inj[a-zA-Z0-9]+ not found/) !== null) {
  220. console.error(e);
  221. throw new Error("Please check the mnemonic");
  222. }
  223. if (
  224. e.message.match(/insufficient/) !== null &&
  225. e.message.match(/funds/) !== null
  226. ) {
  227. console.error(e);
  228. throw new Error("Insufficient funds");
  229. }
  230. console.error("Error executing messages");
  231. console.log(e);
  232. }
  233. }
  234. }