@@ -6,10 +6,11 @@ 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 { TimestampInSec } from "./helpers" ;
10
- import { PriceStore } from "./listen" ;
9
+ import { removeLeading0x , TimestampInSec } from "./helpers" ;
10
+ import { PriceStore , VaaConfig } from "./listen" ;
11
11
import { logger } from "./logging" ;
12
12
import { PromClient } from "./promClient" ;
13
+ import { retry } from "ts-retry-promise" ;
13
14
14
15
const MORGAN_LOG_FORMAT =
15
16
':remote-addr - :remote-user ":method :url HTTP/:http-version"' +
@@ -27,9 +28,25 @@ export class RestException extends Error {
27
28
static PriceFeedIdNotFound ( notFoundIds : string [ ] ) : RestException {
28
29
return new RestException (
29
30
StatusCodes . BAD_REQUEST ,
30
- `Price Feeds with ids ${ notFoundIds . join ( ", " ) } not found`
31
+ `Price Feed(s) with id(s) ${ notFoundIds . join ( ", " ) } not found. `
31
32
) ;
32
33
}
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
+ } ;
33
50
}
34
51
35
52
export class RestAPI {
@@ -54,6 +71,39 @@ export class RestAPI {
54
71
this . promClient = promClient ;
55
72
}
56
73
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
+
57
107
// Run this function without blocking (`await`) if you want to run it async.
58
108
async createApp ( ) {
59
109
const app = express ( ) ;
@@ -126,43 +176,92 @@ export class RestAPI {
126
176
publish_time : Joi . number ( ) . required ( ) ,
127
177
} ) . required ( ) ,
128
178
} ;
179
+
129
180
app . get (
130
181
"/api/get_vaa" ,
131
182
validate ( getVaaInputSchema ) ,
132
- ( req : Request , res : Response ) => {
183
+ asyncWrapper ( async ( req : Request , res : Response ) => {
133
184
const priceFeedId = req . query . id as string ;
134
185
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 ( ) ;
152
197
} 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 ) ;
159
199
}
160
- }
200
+ } )
161
201
) ;
202
+
162
203
endpoints . push (
163
204
"api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>"
164
205
) ;
165
206
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
+
166
265
const latestPriceFeedsInputSchema : schema = {
167
266
query : Joi . object ( {
168
267
ids : Joi . array ( )
0 commit comments