1
1
use {
2
2
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 ,
10
6
} ,
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 {
16
7
anyhow:: Result ,
17
8
axum:: {
18
9
extract:: State ,
@@ -24,47 +15,57 @@ use {
24
15
Json ,
25
16
} ,
26
17
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
+ } ,
27
30
} ;
28
31
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
+
29
43
pub enum RestError {
30
- InvalidPriceId ,
31
44
UpdateDataNotFound ,
32
45
}
33
46
34
47
impl IntoResponse for RestError {
35
48
fn into_response ( self ) -> Response {
36
49
match self {
37
- RestError :: InvalidPriceId => {
38
- ( StatusCode :: BAD_REQUEST , "Invalid Price Id" ) . into_response ( )
39
- }
40
50
RestError :: UpdateDataNotFound => {
41
51
( StatusCode :: NOT_FOUND , "Update data not found" ) . into_response ( )
42
52
}
43
53
}
44
54
}
45
55
}
46
56
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 > ,
50
61
}
51
62
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
+
56
64
pub async fn latest_vaas (
57
65
State ( state) : State < super :: State > ,
58
- Query ( params) : Query < LatestVaaQueryParams > ,
66
+ Query ( params) : Query < LatestVaasQueryParams > ,
59
67
) -> 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 ( ) ;
68
69
let price_feeds_with_update_data = state
69
70
. store
70
71
. get_price_feeds_with_update_data ( price_ids, RequestTime :: Latest )
@@ -74,27 +75,22 @@ pub async fn latest_vaas(
74
75
. update_data
75
76
. batch_vaa
76
77
. 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
78
80
. collect ( ) ,
79
81
) )
80
82
}
81
83
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 > ,
85
87
}
86
88
87
- /// REST endpoint /latest_vaas?ids[]=...&ids[]=...&ids[]=...
88
89
pub async fn latest_price_feeds (
89
90
State ( state) : State < super :: State > ,
90
- Query ( params) : Query < LatestPriceFeedParams > ,
91
+ Query ( params) : Query < LatestPriceFeedsQueryParams > ,
91
92
) -> 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 ( ) ;
98
94
let price_feeds_with_update_data = state
99
95
. store
100
96
. get_price_feeds_with_update_data ( price_ids, RequestTime :: Latest )
@@ -107,6 +103,91 @@ pub async fn latest_price_feeds(
107
103
) )
108
104
}
109
105
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
+
110
191
// This function implements the `/live` endpoint. It returns a `200` status code. This endpoint is
111
192
// used by the Kubernetes liveness probe.
112
193
pub async fn live ( ) -> Result < impl IntoResponse , std:: convert:: Infallible > {
@@ -116,5 +197,11 @@ pub async fn live() -> Result<impl IntoResponse, std::convert::Infallible> {
116
197
// This is the index page for the REST service. It will list all the available endpoints.
117
198
// TODO: Dynamically generate this list if possible.
118
199
pub 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
+ ] )
120
207
}
0 commit comments