sui_graphql/client/
chain.rs1use sui_graphql_macros::Response;
4use sui_graphql_macros::graphql_query;
5
6use super::Client;
7use crate::error::Error;
8use crate::scalars::BigInt;
9use crate::scalars::DateTime;
10use crate::scalars::Digest;
11
12#[derive(Debug, Clone)]
17#[non_exhaustive]
18pub struct Epoch {
19 pub epoch: u64,
21 pub first_checkpoint: Option<u64>,
23 pub last_checkpoint: Option<u64>,
25 pub epoch_start_timestamp: Option<DateTime>,
27 pub epoch_end_timestamp: Option<DateTime>,
29 pub epoch_total_transactions: Option<u64>,
31 pub reference_gas_price: Option<u64>,
33 pub protocol_version: Option<u64>,
35}
36
37impl Client {
38 pub async fn chain_identifier(&self) -> Result<Digest, Error> {
53 #[derive(Response)]
54 struct Response {
55 #[field(path = "chainIdentifier?")]
56 chain_identifier: Option<Digest>,
57 }
58
59 const QUERY: &str = graphql_query!("query { chainIdentifier }");
60
61 let response = self.query::<Response>(QUERY, serde_json::json!({})).await?;
62
63 response
64 .into_data()
65 .and_then(|d| d.chain_identifier)
66 .ok_or(Error::MissingData("chain identifier"))
67 }
68
69 pub async fn protocol_version(&self) -> Result<u64, Error> {
84 #[derive(Response)]
85 struct Response {
86 #[field(path = "protocolConfigs?.protocolVersion?")]
87 protocol_version: Option<u64>,
88 }
89
90 const QUERY: &str = graphql_query!("query { protocolConfigs { protocolVersion } }");
91
92 let response = self.query::<Response>(QUERY, serde_json::json!({})).await?;
93
94 response
95 .into_data()
96 .and_then(|d| d.protocol_version)
97 .ok_or(Error::MissingData("protocol version"))
98 }
99
100 pub async fn epoch(&self, epoch_id: Option<u64>) -> Result<Option<Epoch>, Error> {
121 #[derive(Response)]
122 struct Response {
123 #[field(path = "epoch?.epochId?")]
124 epoch_id: Option<u64>,
125 #[field(path = "epoch?.protocolConfigs?.protocolVersion?")]
126 protocol_version: Option<u64>,
127 #[field(path = "epoch?.referenceGasPrice?")]
128 reference_gas_price: Option<BigInt>,
129 #[field(path = "epoch?.startTimestamp?")]
130 start_timestamp: Option<DateTime>,
131 #[field(path = "epoch?.endTimestamp?")]
132 end_timestamp: Option<DateTime>,
133 #[field(path = "epoch?.totalTransactions?")]
134 total_transactions: Option<u64>,
135 #[field(path = "epoch?.firstCheckpoint:checkpoints?.nodes?[].sequenceNumber")]
139 first_checkpoint_seq: Option<Vec<u64>>,
140 #[field(path = "epoch?.lastCheckpoint:checkpoints?.nodes?[].sequenceNumber")]
142 last_checkpoint_seq: Option<Vec<u64>>,
143 }
144
145 const QUERY: &str = graphql_query!(
146 "query($epochId: UInt53) {
147 epoch(epochId: $epochId) {
148 epochId
149 protocolConfigs {
150 protocolVersion
151 }
152 referenceGasPrice
153 startTimestamp
154 endTimestamp
155 totalTransactions
156 firstCheckpoint: checkpoints(first: 1) {
157 nodes {
158 sequenceNumber
159 }
160 }
161 lastCheckpoint: checkpoints(last: 1) {
162 nodes {
163 sequenceNumber
164 }
165 }
166 }
167 }"
168 );
169
170 let variables = serde_json::json!({
171 "epochId": epoch_id,
172 });
173
174 let response = self.query::<Response>(QUERY, variables).await?;
175
176 let Some(data) = response.into_data() else {
177 return Ok(None);
178 };
179
180 let Some(epoch) = data.epoch_id else {
181 return Ok(None);
182 };
183
184 let reference_gas_price = data.reference_gas_price.map(|b| b.0);
185
186 let first_checkpoint = data.first_checkpoint_seq.and_then(|v| v.first().copied());
188 let last_checkpoint = data.last_checkpoint_seq.and_then(|v| v.first().copied());
189
190 Ok(Some(Epoch {
191 epoch,
192 first_checkpoint,
193 last_checkpoint,
194 epoch_start_timestamp: data.start_timestamp,
195 epoch_end_timestamp: data.end_timestamp,
196 epoch_total_transactions: data.total_transactions,
197 reference_gas_price,
198 protocol_version: data.protocol_version,
199 }))
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use wiremock::Mock;
207 use wiremock::MockServer;
208 use wiremock::ResponseTemplate;
209 use wiremock::matchers::method;
210 use wiremock::matchers::path;
211
212 #[tokio::test]
213 async fn test_chain_identifier() {
214 let mock_server = MockServer::start().await;
215
216 let expected_digest = Digest::ZERO;
218
219 Mock::given(method("POST"))
220 .and(path("/"))
221 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
222 "data": {
223 "chainIdentifier": expected_digest.to_string()
224 }
225 })))
226 .mount(&mock_server)
227 .await;
228
229 let client = Client::new(&mock_server.uri()).unwrap();
230 let result = client.chain_identifier().await;
231
232 assert!(result.is_ok());
233 assert_eq!(result.unwrap(), expected_digest);
234 }
235
236 #[tokio::test]
237 async fn test_protocol_version() {
238 let mock_server = MockServer::start().await;
239
240 Mock::given(method("POST"))
241 .and(path("/"))
242 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
243 "data": {
244 "protocolConfigs": {
245 "protocolVersion": 70
246 }
247 }
248 })))
249 .mount(&mock_server)
250 .await;
251
252 let client = Client::new(&mock_server.uri()).unwrap();
253 let result = client.protocol_version().await;
254
255 assert!(result.is_ok());
256 assert_eq!(result.unwrap(), 70);
257 }
258
259 #[tokio::test]
260 async fn test_protocol_version_missing() {
261 let mock_server = MockServer::start().await;
262
263 Mock::given(method("POST"))
264 .and(path("/"))
265 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
266 "data": {
267 "protocolConfigs": null
268 }
269 })))
270 .mount(&mock_server)
271 .await;
272
273 let client = Client::new(&mock_server.uri()).unwrap();
274 let result = client.protocol_version().await;
275
276 assert!(result.is_err());
277 assert!(matches!(result, Err(Error::MissingData(_))));
278 }
279
280 #[tokio::test]
281 async fn test_epoch() {
282 let mock_server = MockServer::start().await;
283
284 Mock::given(method("POST"))
285 .and(path("/"))
286 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
287 "data": {
288 "epoch": {
289 "epochId": 500,
290 "protocolConfigs": {
291 "protocolVersion": 70
292 },
293 "referenceGasPrice": "1000",
294 "startTimestamp": "2024-01-15T00:00:00Z",
295 "endTimestamp": null,
296 "totalTransactions": 987654,
297 "firstCheckpoint": {
298 "nodes": [{ "sequenceNumber": 10000 }]
299 },
300 "lastCheckpoint": {
301 "nodes": [{ "sequenceNumber": 22344 }]
302 }
303 }
304 }
305 })))
306 .mount(&mock_server)
307 .await;
308
309 let client = Client::new(&mock_server.uri()).unwrap();
310 let result = client.epoch(None).await;
311
312 assert!(result.is_ok());
313 let epoch = result.unwrap();
314 assert!(epoch.is_some());
315
316 let epoch = epoch.unwrap();
317 assert_eq!(epoch.epoch, 500);
318 assert_eq!(epoch.protocol_version, Some(70));
319 assert_eq!(epoch.reference_gas_price, Some(1000));
320 assert_eq!(epoch.epoch_total_transactions, Some(987654));
321 assert_eq!(epoch.first_checkpoint, Some(10000));
322 assert_eq!(epoch.last_checkpoint, Some(22344));
323 }
324
325 #[tokio::test]
326 async fn test_epoch_by_id() {
327 let mock_server = MockServer::start().await;
328
329 Mock::given(method("POST"))
330 .and(path("/"))
331 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
332 "data": {
333 "epoch": {
334 "epochId": 100,
335 "protocolConfigs": {
336 "protocolVersion": 50
337 },
338 "referenceGasPrice": "750",
339 "startTimestamp": "2023-06-01T00:00:00Z",
340 "endTimestamp": "2023-06-02T00:00:00Z",
341 "totalTransactions": 100000,
342 "firstCheckpoint": {
343 "nodes": [{ "sequenceNumber": 1000 }]
344 },
345 "lastCheckpoint": {
346 "nodes": [{ "sequenceNumber": 5999 }]
347 }
348 }
349 }
350 })))
351 .mount(&mock_server)
352 .await;
353
354 let client = Client::new(&mock_server.uri()).unwrap();
355 let result = client.epoch(Some(100)).await;
356
357 assert!(result.is_ok());
358 let epoch = result.unwrap();
359 assert!(epoch.is_some());
360
361 let epoch = epoch.unwrap();
362 assert_eq!(epoch.epoch, 100);
363 assert_eq!(epoch.protocol_version, Some(50));
364 assert_eq!(epoch.reference_gas_price, Some(750));
365 assert_eq!(epoch.epoch_total_transactions, Some(100000));
366 assert_eq!(epoch.first_checkpoint, Some(1000));
367 assert_eq!(epoch.last_checkpoint, Some(5999));
368 }
369
370 #[tokio::test]
375 async fn test_epoch_with_timestamps() {
376 let mock_server = MockServer::start().await;
377
378 Mock::given(method("POST"))
379 .and(path("/"))
380 .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({
381 "data": {
382 "epoch": {
383 "epochId": 100,
384 "protocolConfigs": {
385 "protocolVersion": 50
386 },
387 "referenceGasPrice": "1000",
388 "startTimestamp": "2024-01-15T00:00:00Z",
389 "endTimestamp": "2024-01-16T00:00:00.123Z",
390 "totalTransactions": 100000,
391 "firstCheckpoint": {
392 "nodes": [{ "sequenceNumber": 1000 }]
393 },
394 "lastCheckpoint": {
395 "nodes": [{ "sequenceNumber": 5999 }]
396 }
397 }
398 }
399 })))
400 .mount(&mock_server)
401 .await;
402
403 let client = Client::new(&mock_server.uri()).unwrap();
404 let result = client.epoch(Some(100)).await;
405
406 assert!(result.is_ok());
407 let epoch = result.unwrap().unwrap();
408
409 assert_eq!(
411 epoch.epoch_start_timestamp,
412 Some("2024-01-15T00:00:00Z".parse::<DateTime>().unwrap())
413 );
414 assert_eq!(
415 epoch.epoch_end_timestamp,
416 Some("2024-01-16T00:00:00.123Z".parse::<DateTime>().unwrap())
417 );
418 }
419}