@@ -6,11 +6,16 @@ import { Server } from "http";
66import { StatusCodes } from "http-status-codes" ;
77import morgan from "morgan" ;
88import fetch from "node-fetch" ;
9+ import {
10+ parseBatchPriceAttestation ,
11+ priceAttestationToPriceFeed ,
12+ } from "@pythnetwork/wormhole-attester-sdk" ;
913import { removeLeading0x , TimestampInSec } from "./helpers" ;
10- import { PriceStore , VaaConfig } from "./listen" ;
14+ import { createPriceInfo , PriceInfo , PriceStore , VaaConfig } from "./listen" ;
1115import { logger } from "./logging" ;
1216import { PromClient } from "./promClient" ;
1317import { retry } from "ts-retry-promise" ;
18+ import { parseVaa } from "@certusone/wormhole-sdk" ;
1419
1520const MORGAN_LOG_FORMAT =
1621 ':remote-addr - :remote-user ":method :url HTTP/:http-version"' +
@@ -71,7 +76,10 @@ export class RestAPI {
7176 this . promClient = promClient ;
7277 }
7378
74- async getVaaWithDbLookup ( priceFeedId : string , publishTime : TimestampInSec ) {
79+ async getVaaWithDbLookup (
80+ priceFeedId : string ,
81+ publishTime : TimestampInSec
82+ ) : Promise < VaaConfig | undefined > {
7583 // Try to fetch the vaa from the local cache
7684 let vaa = this . priceFeedVaaInfo . getVaa ( priceFeedId , publishTime ) ;
7785
@@ -104,6 +112,56 @@ export class RestAPI {
104112 return vaa ;
105113 }
106114
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+
107165 // Run this function without blocking (`await`) if you want to run it async.
108166 async createApp ( ) {
109167 const app = express ( ) ;
@@ -283,21 +341,9 @@ export class RestAPI {
283341 continue ;
284342 }
285343
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+ ) ;
301347 }
302348
303349 if ( notFoundIds . length > 0 ) {
@@ -317,6 +363,62 @@ export class RestAPI {
317363 "api/latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..&verbose=true&binary=true"
318364 ) ;
319365
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+
320422 app . get ( "/api/price_feed_ids" , ( req : Request , res : Response ) => {
321423 const availableIds = this . priceFeedVaaInfo . getPriceIds ( ) ;
322424 res . json ( [ ...availableIds ] ) ;
0 commit comments