@@ -6,11 +6,16 @@ import { Server } from "http";
6
6
import { StatusCodes } from "http-status-codes" ;
7
7
import morgan from "morgan" ;
8
8
import fetch from "node-fetch" ;
9
+ import {
10
+ parseBatchPriceAttestation ,
11
+ priceAttestationToPriceFeed ,
12
+ } from "@pythnetwork/wormhole-attester-sdk" ;
9
13
import { removeLeading0x , TimestampInSec } from "./helpers" ;
10
- import { PriceStore , VaaConfig } from "./listen" ;
14
+ import { createPriceInfo , PriceInfo , PriceStore , VaaConfig } from "./listen" ;
11
15
import { logger } from "./logging" ;
12
16
import { PromClient } from "./promClient" ;
13
17
import { retry } from "ts-retry-promise" ;
18
+ import { parseVaa } from "@certusone/wormhole-sdk" ;
14
19
15
20
const MORGAN_LOG_FORMAT =
16
21
':remote-addr - :remote-user ":method :url HTTP/:http-version"' +
@@ -71,7 +76,10 @@ export class RestAPI {
71
76
this . promClient = promClient ;
72
77
}
73
78
74
- async getVaaWithDbLookup ( priceFeedId : string , publishTime : TimestampInSec ) {
79
+ async getVaaWithDbLookup (
80
+ priceFeedId : string ,
81
+ publishTime : TimestampInSec
82
+ ) : Promise < VaaConfig | undefined > {
75
83
// Try to fetch the vaa from the local cache
76
84
let vaa = this . priceFeedVaaInfo . getVaa ( priceFeedId , publishTime ) ;
77
85
@@ -104,6 +112,56 @@ export class RestAPI {
104
112
return vaa ;
105
113
}
106
114
115
+ vaaToPriceInfo ( priceFeedId : string , vaa : Buffer ) : PriceInfo | undefined {
116
+ const parsedVaa = parseVaa ( vaa ) ;
117
+
118
+ let batchAttestation ;
119
+
120
+ try {
121
+ batchAttestation = parseBatchPriceAttestation (
122
+ Buffer . from ( parsedVaa . payload )
123
+ ) ;
124
+ } catch ( e : any ) {
125
+ logger . error ( e , e . stack ) ;
126
+ logger . error ( "Parsing historical VAA failed: %o" , parsedVaa ) ;
127
+ return undefined ;
128
+ }
129
+
130
+ for ( const priceAttestation of batchAttestation . priceAttestations ) {
131
+ if ( priceAttestation . priceId === priceFeedId ) {
132
+ return createPriceInfo (
133
+ priceAttestation ,
134
+ vaa ,
135
+ parsedVaa . sequence ,
136
+ parsedVaa . emitterChain
137
+ ) ;
138
+ }
139
+ }
140
+
141
+ return undefined ;
142
+ }
143
+
144
+ priceInfoToJson (
145
+ priceInfo : PriceInfo ,
146
+ verbose : boolean ,
147
+ binary : boolean
148
+ ) : object {
149
+ return {
150
+ ...priceInfo . priceFeed . toJson ( ) ,
151
+ ...( verbose && {
152
+ metadata : {
153
+ emitter_chain : priceInfo . emitterChainId ,
154
+ attestation_time : priceInfo . attestationTime ,
155
+ sequence_number : priceInfo . seqNum ,
156
+ price_service_receive_time : priceInfo . priceServiceReceiveTime ,
157
+ } ,
158
+ } ) ,
159
+ ...( binary && {
160
+ vaa : priceInfo . vaa . toString ( "base64" ) ,
161
+ } ) ,
162
+ } ;
163
+ }
164
+
107
165
// Run this function without blocking (`await`) if you want to run it async.
108
166
async createApp ( ) {
109
167
const app = express ( ) ;
@@ -283,21 +341,9 @@ export class RestAPI {
283
341
continue ;
284
342
}
285
343
286
- responseJson . push ( {
287
- ...latestPriceInfo . priceFeed . toJson ( ) ,
288
- ...( verbose && {
289
- metadata : {
290
- emitter_chain : latestPriceInfo . emitterChainId ,
291
- attestation_time : latestPriceInfo . attestationTime ,
292
- sequence_number : latestPriceInfo . seqNum ,
293
- price_service_receive_time :
294
- latestPriceInfo . priceServiceReceiveTime ,
295
- } ,
296
- } ) ,
297
- ...( binary && {
298
- vaa : latestPriceInfo . vaa . toString ( "base64" ) ,
299
- } ) ,
300
- } ) ;
344
+ responseJson . push (
345
+ this . priceInfoToJson ( latestPriceInfo , verbose , binary )
346
+ ) ;
301
347
}
302
348
303
349
if ( notFoundIds . length > 0 ) {
@@ -317,6 +363,62 @@ export class RestAPI {
317
363
"api/latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..&verbose=true&binary=true"
318
364
) ;
319
365
366
+ const getPriceFeedInputSchema : schema = {
367
+ query : Joi . object ( {
368
+ id : Joi . string ( )
369
+ . regex ( / ^ ( 0 x ) ? [ a - f 0 - 9 ] { 64 } $ / )
370
+ . required ( ) ,
371
+ publish_time : Joi . number ( ) . required ( ) ,
372
+ verbose : Joi . boolean ( ) ,
373
+ binary : Joi . boolean ( ) ,
374
+ } ) . required ( ) ,
375
+ } ;
376
+
377
+ app . get (
378
+ "/api/get_price_feed" ,
379
+ validate ( getPriceFeedInputSchema ) ,
380
+ asyncWrapper ( async ( req : Request , res : Response ) => {
381
+ const priceFeedId = removeLeading0x ( req . query . id as string ) ;
382
+ const publishTime = Number ( req . query . publish_time as string ) ;
383
+ // verbose is optional, default to false
384
+ const verbose = req . query . verbose === "true" ;
385
+ // binary is optional, default to false
386
+ const binary = req . query . binary === "true" ;
387
+
388
+ if (
389
+ this . priceFeedVaaInfo . getLatestPriceInfo ( priceFeedId ) === undefined
390
+ ) {
391
+ throw RestException . PriceFeedIdNotFound ( [ priceFeedId ] ) ;
392
+ }
393
+
394
+ const vaa = await this . getVaaWithDbLookup ( priceFeedId , publishTime ) ;
395
+ if ( vaa === undefined ) {
396
+ throw RestException . VaaNotFound ( ) ;
397
+ }
398
+
399
+ const priceInfo = this . vaaToPriceInfo (
400
+ priceFeedId ,
401
+ Buffer . from ( vaa . vaa , "base64" )
402
+ ) ;
403
+
404
+ if ( priceInfo === undefined ) {
405
+ throw RestException . VaaNotFound ( ) ;
406
+ } else {
407
+ res . json ( this . priceInfoToJson ( priceInfo , verbose , binary ) ) ;
408
+ }
409
+ } )
410
+ ) ;
411
+
412
+ endpoints . push (
413
+ "api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>"
414
+ ) ;
415
+ endpoints . push (
416
+ "api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>&verbose=true"
417
+ ) ;
418
+ endpoints . push (
419
+ "api/get_price_feed?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>&binary=true"
420
+ ) ;
421
+
320
422
app . get ( "/api/price_feed_ids" , ( req : Request , res : Response ) => {
321
423
const availableIds = this . priceFeedVaaInfo . getPriceIds ( ) ;
322
424
res . json ( [ ...availableIds ] ) ;
0 commit comments