1
1
import { HexString , HermesClient } from "@pythnetwork/hermes-client" ;
2
2
import {
3
- IPricePusher ,
3
+ PriceItem ,
4
4
PriceInfo ,
5
+ IPricePusher ,
5
6
ChainPriceListener ,
6
- PriceItem ,
7
7
} from "../interface" ;
8
8
import { DurationInSeconds } from "../utils" ;
9
9
import {
@@ -37,9 +37,11 @@ type PriceQueryResponse = {
37
37
} ;
38
38
} ;
39
39
40
- type UpdateFeeResponse = {
41
- denom : string ;
42
- amount : string ;
40
+ type InjectiveConfig = {
41
+ chainId : string ;
42
+ gasMultiplier : number ;
43
+ gasPrice : number ;
44
+ priceIdsProcessChunkSize : number ;
43
45
} ;
44
46
45
47
// this use price without leading 0x
@@ -88,16 +90,11 @@ export class InjectivePriceListener extends ChainPriceListener {
88
90
}
89
91
}
90
92
91
- type InjectiveConfig = {
92
- chainId : string ;
93
- gasMultiplier : number ;
94
- gasPrice : number ;
95
- priceIdsProcessChunkSize : number ;
96
- } ;
97
93
export class InjectivePricePusher implements IPricePusher {
98
- private wallet : PrivateKey ;
94
+ private mnemonic : string ;
99
95
private chainConfig : InjectiveConfig ;
100
- private account : Account | null = null ;
96
+ private accounts : Record < string , Account | undefined > =
97
+ { } ; /** { address: Account } */
101
98
102
99
constructor (
103
100
private hermesClient : HermesClient ,
@@ -107,8 +104,7 @@ export class InjectivePricePusher implements IPricePusher {
107
104
mnemonic : string ,
108
105
chainConfig ?: Partial < InjectiveConfig > ,
109
106
) {
110
- this . wallet = PrivateKey . fromMnemonic ( mnemonic ) ;
111
-
107
+ this . mnemonic = mnemonic ;
112
108
this . chainConfig = {
113
109
chainId : chainConfig ?. chainId ?? INJECTIVE_TESTNET_CHAIN_ID ,
114
110
gasMultiplier : chainConfig ?. gasMultiplier ?? DEFAULT_GAS_MULTIPLIER ,
@@ -119,90 +115,64 @@ export class InjectivePricePusher implements IPricePusher {
119
115
} ;
120
116
}
121
117
122
- private injectiveAddress ( ) : string {
123
- return this . wallet . toBech32 ( ) ;
118
+ private getWallet ( index : number ) {
119
+ if (
120
+ this . chainConfig . priceIdsProcessChunkSize === - 1 ||
121
+ this . chainConfig . priceIdsProcessChunkSize === undefined
122
+ ) {
123
+ return PrivateKey . fromMnemonic ( this . mnemonic ) ;
124
+ }
125
+
126
+ return PrivateKey . fromMnemonic ( this . mnemonic , `m/44'/60'/0'/0/${ index } ` ) ;
124
127
}
125
128
126
- private async signAndBroadcastMsg ( msg : Msgs ) : Promise < TxResponse > {
129
+ private async signAndBroadcastMsg (
130
+ msg : Msgs ,
131
+ index : number ,
132
+ ) : Promise < TxResponse > {
127
133
const chainGrpcAuthApi = new ChainGrpcAuthApi ( this . grpcEndpoint ) ;
134
+ const wallet = this . getWallet ( index ) ;
135
+ const injectiveAddress = wallet . toAddress ( ) . toBech32 ( ) ;
128
136
129
137
// Fetch the latest account details only if it's not stored.
130
- this . account ??= await chainGrpcAuthApi . fetchAccount (
131
- this . injectiveAddress ( ) ,
132
- ) ;
138
+ this . accounts [ injectiveAddress ] ??=
139
+ await chainGrpcAuthApi . fetchAccount ( injectiveAddress ) ;
133
140
134
- const { txRaw : simulateTxRaw } = createTransactionFromMsg ( {
135
- sequence : this . account . baseAccount . sequence ,
136
- accountNumber : this . account . baseAccount . accountNumber ,
137
- message : msg ,
138
- chainId : this . chainConfig . chainId ,
139
- pubKey : this . wallet . toPublicKey ( ) . toBase64 ( ) ,
140
- } ) ;
141
+ const account = this . accounts [ injectiveAddress ] ;
141
142
142
- const txService = new TxGrpcApi ( this . grpcEndpoint ) ;
143
- // simulation
144
143
try {
145
- const {
146
- gasInfo : { gasUsed } ,
147
- } = await txService . simulate ( simulateTxRaw ) ;
148
-
149
- // simulation returns us the approximate gas used
150
- // gas passed with the transaction should be more than that
151
- // in order for it to be successfully executed
152
- // this multiplier takes care of that
153
- const gas = ( gasUsed * this . chainConfig . gasMultiplier ) . toFixed ( ) ;
154
- const fee = {
155
- amount : [
156
- {
157
- denom : "inj" ,
158
- amount : ( Number ( gas ) * this . chainConfig . gasPrice ) . toFixed ( ) ,
159
- } ,
160
- ] ,
161
- gas,
162
- } ;
163
-
164
144
const { signBytes, txRaw } = createTransactionFromMsg ( {
165
- sequence : this . account . baseAccount . sequence ,
166
- accountNumber : this . account . baseAccount . accountNumber ,
145
+ sequence : account . baseAccount . sequence ,
146
+ accountNumber : account . baseAccount . accountNumber ,
167
147
message : msg ,
168
148
chainId : this . chainConfig . chainId ,
169
- fee,
170
- pubKey : this . wallet . toPublicKey ( ) . toBase64 ( ) ,
149
+ fee : await this . getStdFee ( msg , index ) ,
150
+ pubKey : wallet . toPublicKey ( ) . toBase64 ( ) ,
171
151
} ) ;
172
152
173
- const sig = await this . wallet . sign ( Buffer . from ( signBytes ) ) ;
174
-
175
- this . account . baseAccount . sequence ++ ;
153
+ const sig = await wallet . sign ( Buffer . from ( signBytes ) ) ;
176
154
177
155
/** Append Signatures */
178
156
txRaw . signatures = [ sig ] ;
157
+
179
158
// this takes approx 5 seconds
180
- const txResponse = await txService . broadcast ( txRaw ) ;
159
+ const txResponse = await new TxGrpcApi ( this . grpcEndpoint ) . broadcast (
160
+ txRaw ,
161
+ ) ;
162
+
163
+ account . baseAccount . sequence ++ ;
181
164
182
165
return txResponse ;
183
166
} catch ( e : any ) {
184
- // The sequence number was invalid and hence we will have to fetch it again.
167
+ // The sequence number was invalid and hence we will have to fetch it again
185
168
if ( JSON . stringify ( e ) . match ( / a c c o u n t s e q u e n c e m i s m a t c h / ) !== null ) {
186
- // We need to fetch the account details again.
187
- this . account = null ;
169
+ this . accounts [ injectiveAddress ] = undefined ;
188
170
}
171
+
189
172
throw e ;
190
173
}
191
174
}
192
175
193
- async getPriceFeedUpdateObject ( priceIds : string [ ] ) : Promise < any > {
194
- const response = await this . hermesClient . getLatestPriceUpdates ( priceIds , {
195
- encoding : "base64" ,
196
- } ) ;
197
- const vaas = response . binary . data ;
198
-
199
- return {
200
- update_price_feeds : {
201
- data : vaas ,
202
- } ,
203
- } ;
204
- }
205
-
206
176
async updatePriceFeed (
207
177
priceIds : string [ ] ,
208
178
pubTimesToPush : number [ ] ,
@@ -222,69 +192,42 @@ export class InjectivePricePusher implements IPricePusher {
222
192
chunkSize : Number ( this . chainConfig . priceIdsProcessChunkSize ) ,
223
193
} ) ;
224
194
225
- for ( const [ chunkIndex , priceIdChunk ] of priceIdChunks . entries ( ) ) {
226
- await this . updatePriceFeedChunk ( priceIdChunk , chunkIndex ) ;
227
- }
195
+ await Promise . all (
196
+ priceIdChunks . map ( ( priceIdChunk , chunkIndex ) =>
197
+ this . updatePriceFeedChunk ( priceIdChunk , chunkIndex ) ,
198
+ ) ,
199
+ ) ;
228
200
}
229
201
230
202
private async updatePriceFeedChunk (
231
203
priceIds : string [ ] ,
232
204
chunkIndex : number ,
233
205
) : Promise < void > {
234
- let priceFeedUpdateObject ;
235
-
236
206
try {
237
- // get the latest VAAs for updatePriceFeed and then push them
238
- priceFeedUpdateObject = await this . getPriceFeedUpdateObject ( priceIds ) ;
239
- } catch ( err ) {
240
- this . logger . error (
241
- err ,
242
- `Error fetching the latest vaas to push for chunk ${ chunkIndex } ` ,
243
- ) ;
244
- return ;
245
- }
246
-
247
- let updateFeeQueryResponse : UpdateFeeResponse ;
248
- try {
249
- const api = new ChainGrpcWasmApi ( this . grpcEndpoint ) ;
250
- const { data } = await api . fetchSmartContractState (
251
- this . pythContractAddress ,
252
- Buffer . from (
253
- JSON . stringify ( {
254
- get_update_fee : {
255
- vaas : priceFeedUpdateObject . update_price_feeds . data ,
256
- } ,
257
- } ) ,
258
- ) . toString ( "base64" ) ,
207
+ const priceFeedUpdateObject =
208
+ await this . getPriceFeedUpdateObject ( priceIds ) ;
209
+ const updateFeeQueryResponse = await this . getUpdateFee (
210
+ priceFeedUpdateObject . update_price_feeds . data ,
259
211
) ;
212
+ const wallet = this . getWallet ( chunkIndex ) ;
260
213
261
- const json = Buffer . from ( data ) . toString ( ) ;
262
- updateFeeQueryResponse = JSON . parse ( json ) ;
263
- } catch ( err ) {
264
- this . logger . error (
265
- err ,
266
- `Error fetching update fee for chunk ${ chunkIndex } ` ,
267
- ) ;
268
- // Throwing an error because it is likely an RPC issue
269
- throw err ;
270
- }
271
-
272
- try {
273
- const executeMsg = MsgExecuteContract . fromJSON ( {
274
- sender : this . injectiveAddress ( ) ,
214
+ const msg = MsgExecuteContract . fromJSON ( {
215
+ sender : wallet . toAddress ( ) . toBech32 ( ) ,
275
216
contractAddress : this . pythContractAddress ,
276
217
msg : priceFeedUpdateObject ,
277
218
funds : [ updateFeeQueryResponse ] ,
278
219
} ) ;
279
220
280
- const rs = await this . signAndBroadcastMsg ( executeMsg ) ;
221
+ const rs = await this . signAndBroadcastMsg ( msg , chunkIndex ) ;
222
+
281
223
this . logger . info (
282
224
{ hash : rs . txHash } ,
283
225
`Successfully broadcasted txHash for chunk ${ chunkIndex } ` ,
284
226
) ;
285
227
} catch ( err : any ) {
286
228
if ( err . message . match ( / a c c o u n t i n j [ a - z A - Z 0 - 9 ] + n o t f o u n d / ) !== null ) {
287
229
this . logger . error ( err , `Account not found for chunk ${ chunkIndex } ` ) ;
230
+
288
231
throw new Error ( "Please check the mnemonic" ) ;
289
232
}
290
233
@@ -295,10 +238,112 @@ export class InjectivePricePusher implements IPricePusher {
295
238
this . logger . error ( err , `Insufficient funds for chunk ${ chunkIndex } ` ) ;
296
239
throw new Error ( "Insufficient funds" ) ;
297
240
}
241
+
298
242
this . logger . error (
299
243
err ,
300
244
`Error executing messages for chunk ${ chunkIndex } ` ,
301
245
) ;
302
246
}
303
247
}
248
+
249
+ /**
250
+ * Get the fee for the transaction (using simulation).
251
+ *
252
+ * We also apply a multiplier to the gas used to apply a small
253
+ * buffer to the gas that'll be used.
254
+ */
255
+ private async getStdFee ( msg : Msgs , index : number ) {
256
+ const wallet = this . getWallet ( index ) ;
257
+ const injectiveAddress = wallet . toAddress ( ) . toBech32 ( ) ;
258
+ const account = this . accounts [ injectiveAddress ] ;
259
+
260
+ if ( ! account ) {
261
+ throw new Error ( "Account not found" ) ;
262
+ }
263
+
264
+ const { txRaw : simulateTxRaw } = createTransactionFromMsg ( {
265
+ sequence : account . baseAccount . sequence ,
266
+ accountNumber : account . baseAccount . accountNumber ,
267
+ message : msg ,
268
+ chainId : this . chainConfig . chainId ,
269
+ pubKey : wallet . toPublicKey ( ) . toBase64 ( ) ,
270
+ } ) ;
271
+
272
+ try {
273
+ const result = await new TxGrpcApi ( this . grpcEndpoint ) . simulate (
274
+ simulateTxRaw ,
275
+ ) ;
276
+
277
+ const gas = (
278
+ result . gasInfo . gasUsed * this . chainConfig . gasMultiplier
279
+ ) . toFixed ( ) ;
280
+ const fee = {
281
+ amount : [
282
+ {
283
+ denom : "inj" ,
284
+ amount : ( Number ( gas ) * this . chainConfig . gasPrice ) . toFixed ( ) ,
285
+ } ,
286
+ ] ,
287
+ gas,
288
+ } ;
289
+
290
+ return fee ;
291
+ } catch ( err ) {
292
+ this . logger . error ( err , `Error getting std fee` ) ;
293
+ throw err ;
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Get the latest VAAs for updatePriceFeed and then push them
299
+ */
300
+ private async getPriceFeedUpdateObject ( priceIds : string [ ] ) {
301
+ try {
302
+ const response = await this . hermesClient . getLatestPriceUpdates ( priceIds , {
303
+ encoding : "base64" ,
304
+ } ) ;
305
+ const vaas = response . binary . data ;
306
+
307
+ return {
308
+ update_price_feeds : {
309
+ data : vaas ,
310
+ } ,
311
+ } as {
312
+ update_price_feeds : {
313
+ data : string [ ] ;
314
+ } ;
315
+ } ;
316
+ } catch ( err ) {
317
+ this . logger . error ( err , `Error fetching the latest vaas to push` ) ;
318
+ throw err ;
319
+ }
320
+ }
321
+
322
+ /**
323
+ * Get the update fee for the given VAAs (i.e the fee that is paid to the pyth contract)
324
+ */
325
+ private async getUpdateFee ( vaas : string [ ] ) {
326
+ try {
327
+ const api = new ChainGrpcWasmApi ( this . grpcEndpoint ) ;
328
+ const { data } = await api . fetchSmartContractState (
329
+ this . pythContractAddress ,
330
+ Buffer . from (
331
+ JSON . stringify ( {
332
+ get_update_fee : {
333
+ vaas,
334
+ } ,
335
+ } ) ,
336
+ ) . toString ( "base64" ) ,
337
+ ) ;
338
+
339
+ const json = Buffer . from ( data ) . toString ( ) ;
340
+
341
+ return JSON . parse ( json ) ;
342
+ } catch ( err ) {
343
+ this . logger . error ( err , `Error fetching update fee.` ) ;
344
+
345
+ // Throwing an error because it is likely an RPC issue
346
+ throw err ;
347
+ }
348
+ }
304
349
}
0 commit comments