@@ -6,10 +6,11 @@ import { Server } from "http";
66import { StatusCodes } from "http-status-codes" ;
77import morgan from "morgan" ;
88import fetch from "node-fetch" ;
9- import { TimestampInSec } from "./helpers" ;
10- import { PriceStore } from "./listen" ;
9+ import { removeLeading0x , TimestampInSec } from "./helpers" ;
10+ import { PriceStore , VaaConfig } from "./listen" ;
1111import { logger } from "./logging" ;
1212import { PromClient } from "./promClient" ;
13+ import { retry } from "ts-retry-promise" ;
1314
1415const MORGAN_LOG_FORMAT =
1516 ':remote-addr - :remote-user ":method :url HTTP/:http-version"' +
@@ -27,9 +28,25 @@ export class RestException extends Error {
2728 static PriceFeedIdNotFound ( notFoundIds : string [ ] ) : RestException {
2829 return new RestException (
2930 StatusCodes . BAD_REQUEST ,
30- `Price Feeds with ids ${ notFoundIds . join ( ", " ) } not found`
31+ `Price Feed(s) with id(s) ${ notFoundIds . join ( ", " ) } not found. `
3132 ) ;
3233 }
34+
35+ static DbApiError ( ) : RestException {
36+ return new RestException ( StatusCodes . INTERNAL_SERVER_ERROR , `DB API Error` ) ;
37+ }
38+
39+ static VaaNotFound ( ) : RestException {
40+ return new RestException ( StatusCodes . NOT_FOUND , "VAA not found." ) ;
41+ }
42+ }
43+
44+ function asyncWrapper (
45+ callback : ( req : Request , res : Response , next : NextFunction ) => Promise < any >
46+ ) {
47+ return function ( req : Request , res : Response , next : NextFunction ) {
48+ callback ( req , res , next ) . catch ( next ) ;
49+ } ;
3350}
3451
3552export class RestAPI {
@@ -54,6 +71,39 @@ export class RestAPI {
5471 this . promClient = promClient ;
5572 }
5673
74+ async getVaaWithDbLookup ( priceFeedId : string , publishTime : TimestampInSec ) {
75+ // Try to fetch the vaa from the local cache
76+ let vaa = this . priceFeedVaaInfo . getVaa ( priceFeedId , publishTime ) ;
77+
78+ // if publishTime is older than cache ttl or vaa is not found, fetch from db
79+ if ( vaa === undefined && this . dbApiEndpoint && this . dbApiCluster ) {
80+ const priceFeedWithoutLeading0x = removeLeading0x ( priceFeedId ) ;
81+
82+ try {
83+ const data = ( await retry (
84+ ( ) =>
85+ fetch (
86+ `${ this . dbApiEndpoint } /vaa?id=${ priceFeedWithoutLeading0x } &publishTime=${ publishTime } &cluster=${ this . dbApiCluster } `
87+ ) . then ( ( res ) => res . json ( ) ) ,
88+ { retries : 3 }
89+ ) ) as any [ ] ;
90+ if ( data . length > 0 ) {
91+ vaa = {
92+ vaa : data [ 0 ] . vaa ,
93+ publishTime : Math . floor (
94+ new Date ( data [ 0 ] . publishTime ) . getTime ( ) / 1000
95+ ) ,
96+ } ;
97+ }
98+ } catch ( e : any ) {
99+ logger . error ( `DB API Error: ${ e } ` ) ;
100+ throw RestException . DbApiError ( ) ;
101+ }
102+ }
103+
104+ return vaa ;
105+ }
106+
57107 // Run this function without blocking (`await`) if you want to run it async.
58108 async createApp ( ) {
59109 const app = express ( ) ;
@@ -126,43 +176,92 @@ export class RestAPI {
126176 publish_time : Joi . number ( ) . required ( ) ,
127177 } ) . required ( ) ,
128178 } ;
179+
129180 app . get (
130181 "/api/get_vaa" ,
131182 validate ( getVaaInputSchema ) ,
132- ( req : Request , res : Response ) => {
183+ asyncWrapper ( async ( req : Request , res : Response ) => {
133184 const priceFeedId = req . query . id as string ;
134185 const publishTime = Number ( req . query . publish_time as string ) ;
135- const vaa = this . priceFeedVaaInfo . getVaa ( priceFeedId , publishTime ) ;
136- // if publishTime is older than cache ttl or vaa is not found, fetch from db
137- if ( ! vaa ) {
138- // cache miss
139- if ( this . dbApiEndpoint && this . dbApiCluster ) {
140- fetch (
141- `${ this . dbApiEndpoint } /vaa?id=${ priceFeedId } &publishTime=${ publishTime } &cluster=${ this . dbApiCluster } `
142- )
143- . then ( ( r : any ) => r . json ( ) )
144- . then ( ( arr : any ) => {
145- if ( arr . length > 0 && arr [ 0 ] ) {
146- res . json ( arr [ 0 ] ) ;
147- } else {
148- res . status ( StatusCodes . NOT_FOUND ) . send ( "VAA not found" ) ;
149- }
150- } ) ;
151- }
186+
187+ if (
188+ this . priceFeedVaaInfo . getLatestPriceInfo ( priceFeedId ) === undefined
189+ ) {
190+ throw RestException . PriceFeedIdNotFound ( [ priceFeedId ] ) ;
191+ }
192+
193+ const vaa = await this . getVaaWithDbLookup ( priceFeedId , publishTime ) ;
194+
195+ if ( vaa === undefined ) {
196+ throw RestException . VaaNotFound ( ) ;
152197 } else {
153- // cache hit
154- const processedVaa = {
155- publishTime : new Date ( vaa . publishTime ) ,
156- vaa : vaa . vaa ,
157- } ;
158- res . json ( processedVaa ) ;
198+ res . json ( vaa ) ;
159199 }
160- }
200+ } )
161201 ) ;
202+
162203 endpoints . push (
163204 "api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>"
164205 ) ;
165206
207+ const getVaaCcipInputSchema : schema = {
208+ query : Joi . object ( {
209+ data : Joi . string ( )
210+ . regex ( / ^ 0 x [ a - f 0 - 9 ] { 80 } $ / )
211+ . required ( ) ,
212+ } ) . required ( ) ,
213+ } ;
214+
215+ // CCIP compatible endpoint. Read more information about it from
216+ // https://eips.ethereum.org/EIPS/eip-3668
217+ app . get (
218+ "/api/get_vaa_ccip" ,
219+ validate ( getVaaCcipInputSchema ) ,
220+ asyncWrapper ( async ( req : Request , res : Response ) => {
221+ const dataHex = req . query . data as string ;
222+ const data = Buffer . from ( removeLeading0x ( dataHex ) , "hex" ) ;
223+
224+ const priceFeedId = data . slice ( 0 , 32 ) . toString ( "hex" ) ;
225+ const publishTime = Number ( data . readBigInt64BE ( 32 ) ) ;
226+
227+ if (
228+ this . priceFeedVaaInfo . getLatestPriceInfo ( priceFeedId ) === undefined
229+ ) {
230+ throw RestException . PriceFeedIdNotFound ( [ priceFeedId ] ) ;
231+ }
232+
233+ const vaa = await this . getVaaWithDbLookup ( priceFeedId , publishTime ) ;
234+
235+ if ( vaa === undefined ) {
236+ // Returning Bad Gateway error because CCIP expects a 5xx error if it needs to
237+ // retry or try other endpoints. Bad Gateway seems the best choice here as this
238+ // is not an internal error and could happen on two scenarios:
239+ // 1. DB Api is not responding well (Bad Gateway is appropriate here)
240+ // 2. Publish time is a few seconds before current time and a VAA
241+ // Will be available in a few seconds. So we want the client to retry.
242+ res
243+ . status ( StatusCodes . BAD_GATEWAY )
244+ . json ( { "message:" : "VAA not found." } ) ;
245+ } else {
246+ const pubTimeBuffer = Buffer . alloc ( 8 ) ;
247+ pubTimeBuffer . writeBigInt64BE ( BigInt ( vaa . publishTime ) ) ;
248+
249+ const resData =
250+ "0x" +
251+ pubTimeBuffer . toString ( "hex" ) +
252+ Buffer . from ( vaa . vaa , "base64" ) . toString ( "hex" ) ;
253+
254+ res . json ( {
255+ data : resData ,
256+ } ) ;
257+ }
258+ } )
259+ ) ;
260+
261+ endpoints . push (
262+ "api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>"
263+ ) ;
264+
166265 const latestPriceFeedsInputSchema : schema = {
167266 query : Joi . object ( {
168267 ids : Joi . array ( )
0 commit comments