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