1
- import {
2
- ChainId ,
3
- hexToUint8Array ,
4
- uint8ArrayToHex ,
5
- } from "@certusone/wormhole-sdk" ;
1
+ import { ChainId , uint8ArrayToHex } from "@certusone/wormhole-sdk" ;
6
2
7
3
import {
8
4
createSpyRPCServiceClient ,
@@ -11,6 +7,8 @@ import {
11
7
12
8
import { importCoreWasm } from "@certusone/wormhole-sdk/lib/cjs/solana/wasm" ;
13
9
10
+ import { createHash } from "crypto" ;
11
+
14
12
import {
15
13
getBatchSummary ,
16
14
parseBatchPriceAttestation ,
@@ -25,10 +23,12 @@ import { HexString, PriceFeed } from "@pythnetwork/pyth-sdk-js";
25
23
import { sleep , TimestampInSec } from "./helpers" ;
26
24
import { logger } from "./logging" ;
27
25
import { PromClient } from "./promClient" ;
26
+ import LRUCache from "lru-cache" ;
28
27
29
28
export type PriceInfo = {
30
- vaaBytes : string ;
29
+ vaa : Buffer ;
31
30
seqNum : number ;
31
+ publishTime : TimestampInSec ;
32
32
attestationTime : TimestampInSec ;
33
33
priceFeed : PriceFeed ;
34
34
emitterChainId : number ;
@@ -52,6 +52,8 @@ type ListenerConfig = {
52
52
readiness : ListenerReadinessConfig ;
53
53
} ;
54
54
55
+ type VaaHash = string ;
56
+
55
57
export class Listener implements PriceStore {
56
58
// Mapping of Price Feed Id to Vaa
57
59
private priceFeedVaaMap = new Map < string , PriceInfo > ( ) ;
@@ -61,13 +63,18 @@ export class Listener implements PriceStore {
61
63
private spyConnectionTime : TimestampInSec | undefined ;
62
64
private readinessConfig : ListenerReadinessConfig ;
63
65
private updateCallbacks : ( ( priceInfo : PriceInfo ) => any ) [ ] ;
66
+ private observedVaas : LRUCache < VaaHash , boolean > ;
64
67
65
68
constructor ( config : ListenerConfig , promClient ?: PromClient ) {
66
69
this . promClient = promClient ;
67
70
this . spyServiceHost = config . spyServiceHost ;
68
71
this . loadFilters ( config . filtersRaw ) ;
69
72
this . readinessConfig = config . readiness ;
70
73
this . updateCallbacks = [ ] ;
74
+ this . observedVaas = new LRUCache ( {
75
+ max : 10000 , // At most 10000 items
76
+ ttl : 60 * 1000 , // 60 seconds
77
+ } ) ;
71
78
}
72
79
73
80
private loadFilters ( filtersRaw ?: string ) {
@@ -114,7 +121,7 @@ export class Listener implements PriceStore {
114
121
) ;
115
122
stream = await subscribeSignedVAA ( client , { filters : this . filters } ) ;
116
123
117
- stream ! . on ( "data" , ( { vaaBytes } : { vaaBytes : string } ) => {
124
+ stream ! . on ( "data" , ( { vaaBytes } : { vaaBytes : Buffer } ) => {
118
125
this . processVaa ( vaaBytes ) ;
119
126
} ) ;
120
127
@@ -150,19 +157,29 @@ export class Listener implements PriceStore {
150
157
}
151
158
}
152
159
153
- async processVaa ( vaaBytes : string ) {
160
+ async processVaa ( vaa : Buffer ) {
154
161
const { parse_vaa } = await importCoreWasm ( ) ;
155
- const parsedVAA = parse_vaa ( hexToUint8Array ( vaaBytes ) ) ;
162
+
163
+ const vaaHash : VaaHash = createHash ( "md5" ) . update ( vaa ) . digest ( "base64" ) ;
164
+
165
+ if ( this . observedVaas . has ( vaaHash ) ) {
166
+ return ;
167
+ }
168
+
169
+ this . observedVaas . set ( vaaHash , true ) ;
170
+ this . promClient ?. incReceivedVaa ( ) ;
171
+
172
+ const parsedVaa = parse_vaa ( vaa ) ;
156
173
157
174
let batchAttestation ;
158
175
159
176
try {
160
177
batchAttestation = await parseBatchPriceAttestation (
161
- Buffer . from ( parsedVAA . payload )
178
+ Buffer . from ( parsedVaa . payload )
162
179
) ;
163
180
} catch ( e : any ) {
164
181
logger . error ( e , e . stack ) ;
165
- logger . error ( "Parsing failed. Dropping vaa: %o" , parsedVAA ) ;
182
+ logger . error ( "Parsing failed. Dropping vaa: %o" , parsedVaa ) ;
166
183
return ;
167
184
}
168
185
@@ -194,15 +211,30 @@ export class Listener implements PriceStore {
194
211
) {
195
212
const priceFeed = priceAttestationToPriceFeed ( priceAttestation ) ;
196
213
const priceInfo = {
197
- seqNum : parsedVAA . sequence ,
198
- vaaBytes,
214
+ seqNum : parsedVaa . sequence ,
215
+ vaa,
216
+ publishTime : priceAttestation . publishTime ,
199
217
attestationTime : priceAttestation . attestationTime ,
200
218
priceFeed,
201
- emitterChainId : parsedVAA . emitter_chain ,
219
+ emitterChainId : parsedVaa . emitter_chain ,
202
220
priceServiceReceiveTime : Math . floor ( new Date ( ) . getTime ( ) / 1000 ) ,
203
221
} ;
204
222
this . priceFeedVaaMap . set ( key , priceInfo ) ;
205
223
224
+ if ( lastAttestationTime !== undefined ) {
225
+ this . promClient ?. addPriceUpdatesAttestationTimeGap (
226
+ priceAttestation . attestationTime - lastAttestationTime
227
+ ) ;
228
+ }
229
+
230
+ const lastPublishTime = this . priceFeedVaaMap . get ( key ) ?. publishTime ;
231
+
232
+ if ( lastPublishTime !== undefined ) {
233
+ this . promClient ?. addPriceUpdatesPublishTimeGap (
234
+ priceAttestation . publishTime - lastPublishTime
235
+ ) ;
236
+ }
237
+
206
238
for ( const callback of this . updateCallbacks ) {
207
239
callback ( priceInfo ) ;
208
240
}
@@ -211,16 +243,14 @@ export class Listener implements PriceStore {
211
243
212
244
logger . info (
213
245
"Parsed a new Batch Price Attestation: [" +
214
- parsedVAA . emitter_chain +
246
+ parsedVaa . emitter_chain +
215
247
":" +
216
- uint8ArrayToHex ( parsedVAA . emitter_address ) +
248
+ uint8ArrayToHex ( parsedVaa . emitter_address ) +
217
249
"], seqNum: " +
218
- parsedVAA . sequence +
250
+ parsedVaa . sequence +
219
251
", Batch Summary: " +
220
252
getBatchSummary ( batchAttestation )
221
253
) ;
222
-
223
- this . promClient ?. incReceivedVaa ( ) ;
224
254
}
225
255
226
256
getLatestPriceInfo ( priceFeedId : string ) : PriceInfo | undefined {
0 commit comments