11use {
22 crate :: store:: RequestTime ,
3- base64:: {
4- engine:: general_purpose:: STANDARD as base64_standard_engine,
5- Engine as _,
6- } ,
7- pyth_sdk:: {
8- PriceFeed ,
9- PriceIdentifier ,
3+ crate :: {
4+ impl_deserialize_for_hex_string_wrapper,
5+ store:: UnixTimestamp ,
106 } ,
11- } ;
12- // This file implements a REST service for the Price Service. This is a mostly direct copy of the
13- // TypeScript implementation in the `pyth-crosschain` repo. It uses `axum` as the web framework and
14- // `tokio` as the async runtime.
15- use {
167 anyhow:: Result ,
178 axum:: {
189 extract:: State ,
@@ -24,47 +15,57 @@ use {
2415 Json ,
2516 } ,
2617 axum_extra:: extract:: Query , // Axum extra Query allows us to parse multi-value query parameters.
18+ base64:: {
19+ engine:: general_purpose:: STANDARD as base64_standard_engine,
20+ Engine as _,
21+ } ,
22+ derive_more:: {
23+ Deref ,
24+ DerefMut ,
25+ } ,
26+ pyth_sdk:: {
27+ PriceFeed ,
28+ PriceIdentifier ,
29+ } ,
2730} ;
2831
32+ #[ derive( Debug , Clone , Deref , DerefMut ) ]
33+ pub struct PriceIdInput ( [ u8 ; 32 ] ) ;
34+ // TODO: Use const generics instead of macro.
35+ impl_deserialize_for_hex_string_wrapper ! ( PriceIdInput , 32 ) ;
36+
37+ impl From < PriceIdInput > for PriceIdentifier {
38+ fn from ( id : PriceIdInput ) -> Self {
39+ Self :: new ( * id)
40+ }
41+ }
42+
2943pub enum RestError {
30- InvalidPriceId ,
3144 UpdateDataNotFound ,
3245}
3346
3447impl IntoResponse for RestError {
3548 fn into_response ( self ) -> Response {
3649 match self {
37- RestError :: InvalidPriceId => {
38- ( StatusCode :: BAD_REQUEST , "Invalid Price Id" ) . into_response ( )
39- }
4050 RestError :: UpdateDataNotFound => {
4151 ( StatusCode :: NOT_FOUND , "Update data not found" ) . into_response ( )
4252 }
4353 }
4454 }
4555}
4656
47- #[ derive( Debug , serde:: Serialize , serde:: Deserialize ) ]
48- pub struct LatestVaaQueryParams {
49- ids : Vec < String > ,
57+
58+ #[ derive( Debug , serde:: Deserialize ) ]
59+ pub struct LatestVaasQueryParams {
60+ ids : Vec < PriceIdInput > ,
5061}
5162
52- /// REST endpoint /latest_vaas?ids[]=...&ids[]=...&ids[]=...
53- ///
54- /// TODO: This endpoint returns update data as an array of base64 encoded strings. We want
55- /// to support other formats such as hex in the future.
63+
5664pub async fn latest_vaas (
5765 State ( state) : State < super :: State > ,
58- Query ( params) : Query < LatestVaaQueryParams > ,
66+ Query ( params) : Query < LatestVaasQueryParams > ,
5967) -> Result < Json < Vec < String > > , RestError > {
60- // TODO: Find better ways to validate query parameters.
61- // FIXME: Handle ids with leading 0x
62- let price_ids: Vec < PriceIdentifier > = params
63- . ids
64- . iter ( )
65- . map ( PriceIdentifier :: from_hex)
66- . collect :: < Result < Vec < PriceIdentifier > , _ > > ( )
67- . map_err ( |_| RestError :: InvalidPriceId ) ?;
68+ let price_ids: Vec < PriceIdentifier > = params. ids . into_iter ( ) . map ( |id| id. into ( ) ) . collect ( ) ;
6869 let price_feeds_with_update_data = state
6970 . store
7071 . get_price_feeds_with_update_data ( price_ids, RequestTime :: Latest )
@@ -74,27 +75,22 @@ pub async fn latest_vaas(
7475 . update_data
7576 . batch_vaa
7677 . iter ( )
77- . map ( |vaa_bytes| base64_standard_engine. encode ( vaa_bytes) )
78+ . map ( |vaa_bytes| base64_standard_engine. encode ( vaa_bytes) ) // TODO: Support multiple
79+ // encoding formats
7880 . collect ( ) ,
7981 ) )
8082}
8183
82- #[ derive( Debug , serde:: Serialize , serde :: Deserialize ) ]
83- pub struct LatestPriceFeedParams {
84- ids : Vec < String > ,
84+ #[ derive( Debug , serde:: Deserialize ) ]
85+ pub struct LatestPriceFeedsQueryParams {
86+ ids : Vec < PriceIdInput > ,
8587}
8688
87- /// REST endpoint /latest_vaas?ids[]=...&ids[]=...&ids[]=...
8889pub async fn latest_price_feeds (
8990 State ( state) : State < super :: State > ,
90- Query ( params) : Query < LatestPriceFeedParams > ,
91+ Query ( params) : Query < LatestPriceFeedsQueryParams > ,
9192) -> Result < Json < Vec < PriceFeed > > , RestError > {
92- let price_ids: Vec < PriceIdentifier > = params
93- . ids
94- . iter ( )
95- . map ( PriceIdentifier :: from_hex)
96- . collect :: < Result < Vec < PriceIdentifier > , _ > > ( )
97- . map_err ( |_| RestError :: InvalidPriceId ) ?;
93+ let price_ids: Vec < PriceIdentifier > = params. ids . into_iter ( ) . map ( |id| id. into ( ) ) . collect ( ) ;
9894 let price_feeds_with_update_data = state
9995 . store
10096 . get_price_feeds_with_update_data ( price_ids, RequestTime :: Latest )
@@ -107,6 +103,91 @@ pub async fn latest_price_feeds(
107103 ) )
108104}
109105
106+ #[ derive( Debug , serde:: Deserialize ) ]
107+ pub struct GetVaaQueryParams {
108+ id : PriceIdInput ,
109+ publish_time : UnixTimestamp ,
110+ }
111+
112+ #[ derive( Debug , serde:: Serialize ) ]
113+ pub struct GetVaaResponse {
114+ pub vaa : String ,
115+ #[ serde( rename = "publishTime" ) ]
116+ pub publish_time : UnixTimestamp ,
117+ }
118+
119+ pub async fn get_vaa (
120+ State ( state) : State < super :: State > ,
121+ Query ( params) : Query < GetVaaQueryParams > ,
122+ ) -> Result < Json < GetVaaResponse > , RestError > {
123+ let price_id: PriceIdentifier = params. id . into ( ) ;
124+
125+ let price_feeds_with_update_data = state
126+ . store
127+ . get_price_feeds_with_update_data (
128+ vec ! [ price_id] ,
129+ RequestTime :: FirstAfter ( params. publish_time ) ,
130+ )
131+ . map_err ( |_| RestError :: UpdateDataNotFound ) ?;
132+
133+ let vaa = price_feeds_with_update_data
134+ . update_data
135+ . batch_vaa
136+ . get ( 0 )
137+ . map ( |vaa_bytes| base64_standard_engine. encode ( vaa_bytes) )
138+ . ok_or ( RestError :: UpdateDataNotFound ) ?;
139+
140+ let publish_time = price_feeds_with_update_data
141+ . price_feeds
142+ . get ( & price_id)
143+ . map ( |price_feed| price_feed. get_price_unchecked ( ) . publish_time )
144+ . ok_or ( RestError :: UpdateDataNotFound ) ?;
145+ let publish_time: UnixTimestamp = publish_time
146+ . try_into ( )
147+ . map_err ( |_| RestError :: UpdateDataNotFound ) ?;
148+
149+ Ok ( Json ( GetVaaResponse { vaa, publish_time } ) )
150+ }
151+
152+ #[ derive( Debug , Clone , Deref , DerefMut ) ]
153+ pub struct GetVaaCcipInput ( [ u8 ; 40 ] ) ;
154+ impl_deserialize_for_hex_string_wrapper ! ( GetVaaCcipInput , 40 ) ;
155+
156+ #[ derive( Debug , serde:: Deserialize ) ]
157+ pub struct GetVaaCcipQueryParams {
158+ data : GetVaaCcipInput ,
159+ }
160+
161+ #[ derive( Debug , serde:: Serialize ) ]
162+ pub struct GetVaaCcipResponse {
163+ data : String , // TODO: Use a typed wrapper for the hex output with leading 0x.
164+ }
165+
166+ pub async fn get_vaa_ccip (
167+ State ( state) : State < super :: State > ,
168+ Query ( params) : Query < GetVaaCcipQueryParams > ,
169+ ) -> Result < Json < GetVaaCcipResponse > , RestError > {
170+ let price_id: PriceIdentifier = PriceIdentifier :: new ( params. data [ 0 ..32 ] . try_into ( ) . unwrap ( ) ) ;
171+ let publish_time = UnixTimestamp :: from_be_bytes ( params. data [ 32 ..40 ] . try_into ( ) . unwrap ( ) ) ;
172+
173+ let price_feeds_with_update_data = state
174+ . store
175+ . get_price_feeds_with_update_data ( vec ! [ price_id] , RequestTime :: FirstAfter ( publish_time) )
176+ . map_err ( |_| RestError :: UpdateDataNotFound ) ?;
177+
178+ let vaa = price_feeds_with_update_data
179+ . update_data
180+ . batch_vaa
181+ . get ( 0 ) // One price feed has only a single VAA as proof.
182+ . ok_or ( RestError :: UpdateDataNotFound ) ?;
183+
184+ // FIXME: We should return 5xx when the vaa is not found and 4xx when the price id is not there
185+
186+ Ok ( Json ( GetVaaCcipResponse {
187+ data : format ! ( "0x{}" , hex:: encode( vaa) ) ,
188+ } ) )
189+ }
190+
110191// This function implements the `/live` endpoint. It returns a `200` status code. This endpoint is
111192// used by the Kubernetes liveness probe.
112193pub async fn live ( ) -> Result < impl IntoResponse , std:: convert:: Infallible > {
@@ -116,5 +197,11 @@ pub async fn live() -> Result<impl IntoResponse, std::convert::Infallible> {
116197// This is the index page for the REST service. It will list all the available endpoints.
117198// TODO: Dynamically generate this list if possible.
118199pub async fn index ( ) -> impl IntoResponse {
119- Json ( [ "/live" , "/latest_price_feeds" , "/latest_vaas" ] )
200+ Json ( [
201+ "/live" ,
202+ "/api/latest_price_feeds?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&.." ,
203+ "/api/latest_vaas?ids[]=<price_feed_id>&ids[]=<price_feed_id_2>&..." ,
204+ "/api/get_vaa?id=<price_feed_id>&publish_time=<publish_time_in_unix_timestamp>" ,
205+ "/api/get_vaa_ccip?data=<0x<price_feed_id_32_bytes>+<publish_time_unix_timestamp_be_8_bytes>>" ,
206+ ] )
120207}
0 commit comments