cido_ethereum/
graphql.rs

1use std::borrow::Cow;
2use std::str::FromStr;
3use std::sync::Arc;
4
5use super::EthereumError;
6use super::network::block_info::{EthereumBlockInfo, EthereumBlockInfoOrderBy};
7use super::types::H256;
8use crate::prelude::*;
9use cido::__internal::chrono::{DateTime, Utc};
10use cido::__internal::{
11  BlockId, GraphqlMetaExtension, GraphqlOrderBy, Network, async_graphql, tracing,
12};
13
14impl async_graphql::OutputType for EthereumBlockNumber {
15  fn type_name() -> std::borrow::Cow<'static, str> {
16    <Self as async_graphql::InputType>::type_name()
17  }
18
19  fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String {
20    <Self as async_graphql::InputType>::create_type_info(registry)
21  }
22
23  async fn resolve(
24    &self,
25    _ctx: &async_graphql::ContextSelectionSet<'_>,
26    _field: &async_graphql::Positioned<async_graphql::parser::types::Field>,
27  ) -> async_graphql::ServerResult<async_graphql::Value> {
28    Ok(async_graphql::Value::Number(self.number.into()))
29  }
30}
31
32impl async_graphql::InputType for EthereumBlockNumber {
33  type RawValueType = Self;
34
35  fn type_name() -> std::borrow::Cow<'static, str> {
36    "BlockNumber".into()
37  }
38
39  fn create_type_info(registry: &mut async_graphql::registry::Registry) -> String {
40    registry.create_input_type::<Self, _>(async_graphql::registry::MetaTypeId::Scalar, |_| {
41      async_graphql::registry::MetaType::Scalar {
42        name: Self::type_name().to_string(),
43        description: Some("8 byte signed integer".into()),
44        visible: None,
45        is_valid: None,
46        specified_by_url: None,
47        inaccessible: false,
48        tags: vec![],
49        directive_invocations: vec![],
50      }
51    })
52  }
53
54  fn parse(value: Option<async_graphql::Value>) -> async_graphql::InputValueResult<Self> {
55    if let Some(val) = value {
56      match val {
57        async_graphql::Value::Number(num) => {
58          Ok(Self::new(num.as_u64().ok_or_else(|| {
59            async_graphql::InputValueError::custom("expect u64")
60          })?))
61        }
62        async_graphql::Value::String(val) => val.parse::<u64>().map(Self::new).map_err(|_| {
63          async_graphql::InputValueError::expected_type(async_graphql::Value::String(val))
64        }),
65        val => async_graphql::Result::Err(async_graphql::InputValueError::expected_type(val)),
66      }
67    } else {
68      async_graphql::Result::Err(async_graphql::InputValueError::expected_type(
69        value.unwrap_or_default(),
70      ))
71    }
72  }
73
74  fn to_value(&self) -> async_graphql::Value {
75    async_graphql::Value::Number(self.number.into())
76  }
77
78  fn as_raw_value(&self) -> Option<&Self::RawValueType> {
79    Some(self)
80  }
81}
82
83/// Different ways to refer to a block
84///
85/// Currently supports a block number, a hash, or just "latest"
86#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
87#[non_exhaustive]
88pub enum EthereumBlockId {
89  Number(EthereumBlockNumber),
90  Hash(H256),
91  Latest,
92}
93
94#[crate::_impls(keep)]
95impl BlockId for EthereumBlockId {
96  type Network = EthereumNetwork;
97
98  async fn latest() -> Result<Self, <Self::Network as cido::prelude::Network>::Error> {
99    Ok(Self::Latest)
100  }
101
102  async fn to_block_number(
103    self,
104    network: &EthereumNetwork,
105  ) -> Result<EthereumBlockNumber, EthereumError> {
106    match self {
107      Self::Number(n) => Ok(n),
108      _ => {
109        let header = network.block_info_lookup(self).await?;
110        Ok(header.block_number)
111      }
112    }
113  }
114}
115
116impl Default for EthereumBlockId {
117  fn default() -> Self {
118    Self::Latest
119  }
120}
121
122impl core::fmt::Display for EthereumBlockId {
123  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
124    match self {
125      Self::Number(num) => write!(f, "{}", num),
126      Self::Hash(hash) => write!(f, "{:?}", hash),
127      Self::Latest => write!(f, "Latest"),
128    }
129  }
130}
131
132impl core::fmt::Debug for EthereumBlockId {
133  fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
134    match self {
135      Self::Number(num) => write!(f, "EthereumBlockId::Number({})", num),
136      Self::Hash(hash) => write!(f, "EthereumBlockId::Hash({})", hash),
137      Self::Latest => write!(f, "EthereumBlockId::Latest"),
138    }
139  }
140}
141
142impl async_graphql::InputType for EthereumBlockId {
143  type RawValueType = Self;
144
145  fn type_name() -> Cow<'static, str> {
146    Cow::Borrowed("Block_height")
147  }
148
149  fn create_type_info(
150    registry: &mut ::cido::__internal::async_graphql::registry::Registry,
151  ) -> String {
152    registry.create_input_type::<Self, _>(
153      ::cido::__internal::async_graphql::registry::MetaTypeId::InputObject,
154      |registry| ::cido::__internal::async_graphql::registry::MetaType::InputObject {
155        name: Self::type_name().to_string(),
156        description: None,
157        input_fields: {
158          let mut map = async_graphql::indexmap::IndexMap::new();
159          map.insert("number".into(), async_graphql::registry::MetaInputValue {
160            name: "number".into(),
161            description: None,
162            default_value: None,
163            ty: <Option<u64> as async_graphql::InputType>::create_type_info(registry),
164            visible: None,
165            inaccessible: false,
166            tags: vec![],
167            directive_invocations: vec![],
168            is_secret: false,
169            deprecation: async_graphql::registry::Deprecation::NoDeprecated,
170          });
171
172          map.insert(
173            "number_gte".into(),
174            async_graphql::registry::MetaInputValue {
175              name: "number_gte".into(),
176              description: None,
177              default_value: None,
178              ty: <Option<u64> as async_graphql::InputType>::create_type_info(registry),
179              visible: None,
180              inaccessible: false,
181              tags: vec![],
182              directive_invocations: vec![],
183              is_secret: false,
184              deprecation: async_graphql::registry::Deprecation::NoDeprecated,
185            },
186          );
187
188          map.insert("hash".into(), async_graphql::registry::MetaInputValue {
189            name: "hash".into(),
190            description: None,
191            default_value: None,
192            ty: <Option<H256> as async_graphql::InputType>::create_type_info(registry),
193            visible: None,
194            inaccessible: false,
195            tags: vec![],
196            directive_invocations: vec![],
197            is_secret: false,
198            deprecation: async_graphql::registry::Deprecation::NoDeprecated,
199          });
200
201          map
202        },
203        visible: None,
204        inaccessible: false,
205        tags: vec![],
206        directive_invocations: vec![],
207        rust_typename: Some(::std::any::type_name::<Self>()),
208        oneof: false,
209      },
210    )
211  }
212
213  fn parse(value: Option<async_graphql::Value>) -> async_graphql::InputValueResult<Self> {
214    match value {
215      Some(val) => match val {
216        async_graphql::Value::Null => Ok(Self::Latest),
217        async_graphql::Value::Number(num) => match num.as_u64() {
218          Some(n) => Ok(Self::Number(EthereumBlockNumber::new(n))),
219          None => async_graphql::Result::Err(async_graphql::InputValueError::custom(format!(
220            "unexpected value {}",
221            num
222          ))),
223        },
224        async_graphql::Value::String(v) => <H256 as FromStr>::from_str(&v)
225          .map(Self::Hash)
226          .or_else(|e| {
227            if v.to_lowercase().eq("latest") {
228              Ok(Self::Latest)
229            } else {
230              Err(e)
231            }
232          })
233          .map_err(async_graphql::InputValueError::custom),
234        async_graphql::Value::Binary(bytes) => H256::try_from(&*bytes)
235          .map(Self::Hash)
236          .map_err(async_graphql::InputValueError::custom),
237        async_graphql::Value::Object(obj) => {
238          let number: Option<EthereumBlockNumber> =
239            async_graphql::InputType::parse(obj.get("number").cloned())
240              .map_err(async_graphql::InputValueError::propagate)?;
241          let hash: Option<H256> = async_graphql::InputType::parse(obj.get("hash").cloned())
242            .map_err(async_graphql::InputValueError::propagate)?;
243
244          if number.is_some() && hash.is_some() {
245            return async_graphql::Result::Err(async_graphql::InputValueError::custom(
246              "cannot travel number and hash simultaneously",
247            ));
248          }
249
250          if let Some(number) = number {
251            return async_graphql::Result::Ok(Self::Number(number));
252          }
253
254          if let Some(hash) = hash {
255            return async_graphql::Result::Ok(Self::Hash(hash));
256          }
257
258          async_graphql::Result::Err(async_graphql::InputValueError::expected_type(
259            async_graphql::Value::Object(obj),
260          ))
261        }
262        x => async_graphql::Result::Err(async_graphql::InputValueError::expected_type(x)),
263      },
264      None => async_graphql::Result::Err(async_graphql::InputValueError::expected_type(
265        value.unwrap_or_default(),
266      )),
267    }
268  }
269
270  fn to_value(&self) -> async_graphql::Value {
271    match self {
272      Self::Number(number) => <EthereumBlockNumber as async_graphql::InputType>::to_value(number),
273      Self::Hash(hash) => <H256 as async_graphql::InputType>::to_value(hash),
274      Self::Latest => async_graphql::Value::String("latest".into()),
275    }
276  }
277
278  fn as_raw_value(&self) -> Option<&Self::RawValueType> {
279    Some(self)
280  }
281}
282
283impl From<EthereumBlockNumber> for EthereumBlockId {
284  fn from(block_number: EthereumBlockNumber) -> Self {
285    EthereumBlockId::Number(block_number)
286  }
287}
288
289impl From<H256> for EthereumBlockId {
290  fn from(block_hash: H256) -> Self {
291    EthereumBlockId::Hash(block_hash)
292  }
293}
294
295/// Struct that allows querying full blocks from a node
296#[derive(Default)]
297pub struct FullBlockQuery;
298
299#[crate::_impls(keep)]
300#[async_graphql::Object]
301impl FullBlockQuery {
302  #[allow(clippy::too_many_arguments)]
303  pub async fn full_blocks<'ctx>(
304    &self,
305    ctx: &async_graphql::Context<'ctx>,
306    block: Option<EthereumBlockId>,
307    first: Option<u64>,
308    skip: Option<u64>,
309    timestamp_from: Option<DateTime<Utc>>,
310    timestamp_to: Option<DateTime<Utc>>,
311    order_by: Option<GraphqlOrderBy<EthereumBlockInfoOrderBy>>,
312    order_direction: Option<::cido::__internal::GraphqlOrderDirection>,
313  ) -> async_graphql::Result<Vec<Arc<<EthereumNetwork as Network>::FullBlock>>> {
314    ctx
315      .data::<Arc<EthereumNetwork>>()?
316      .fetch_full_blocks(
317        block,
318        timestamp_from,
319        timestamp_to,
320        order_by,
321        order_direction,
322        first,
323        skip,
324      )
325      .await
326      .map_err(|e| {
327        tracing::error!(err=?e);
328        async_graphql::Error::new("Error fetching full blocks")
329      })
330  }
331}
332
333/// Struct that allows querying blocks from a node
334#[derive(Default)]
335pub struct BlockQuery;
336
337#[crate::_impls(keep)]
338#[async_graphql::Object]
339impl BlockQuery {
340  #[allow(clippy::too_many_arguments)]
341  pub async fn blocks<'ctx>(
342    &self,
343    ctx: &async_graphql::Context<'ctx>,
344    block: Option<EthereumBlockId>,
345    first: Option<u64>,
346    skip: Option<u64>,
347    timestamp_from: Option<DateTime<Utc>>,
348    timestamp_to: Option<DateTime<Utc>>,
349    // timestamp epoch in seconds
350    timestamp_epoch_from: Option<i64>,
351    // timestamp epoch in seconds
352    timestamp_epoch_to: Option<i64>,
353    order_by: Option<GraphqlOrderBy<EthereumBlockInfoOrderBy>>,
354    order_direction: Option<::cido::__internal::GraphqlOrderDirection>,
355  ) -> async_graphql::Result<Vec<EthereumBlockInfo>> {
356    ctx
357      .data::<Arc<EthereumNetwork>>()?
358      .fetch_blocks(
359        block,
360        timestamp_from
361          .or_else(|| timestamp_epoch_from.and_then(|t| DateTime::from_timestamp(t, 0))),
362        timestamp_to.or_else(|| timestamp_epoch_to.and_then(|t| DateTime::from_timestamp(t, 0))),
363        order_by,
364        order_direction,
365        first,
366        skip,
367      )
368      .await
369      .map_err(|e| {
370        tracing::error!(err=?e);
371        async_graphql::Error::new("Error fetching blocks")
372      })
373  }
374}
375
376pub struct EthereumMetaBlock {
377  number: EthereumBlockNumber,
378  hash: H256,
379  timestamp: u64,
380}
381
382#[async_graphql::Object(name = "_Block_")]
383impl EthereumMetaBlock {
384  /// The block number
385  async fn number(&self) -> i32 {
386    self.number.as_u64().try_into().unwrap()
387  }
388  /// The hash of the block
389  async fn hash(&self) -> Option<H256> {
390    (self.hash != H256::default()).then_some(self.hash)
391  }
392  /// Integer representation of the timestamp stored in blocks for the chain
393  async fn timestamp(&self) -> Option<u64> {
394    (self.timestamp != 0).then_some(self.timestamp)
395  }
396}
397
398pub struct EthereumMetaExtension {
399  block: EthereumBlockNumber,
400}
401
402impl GraphqlMetaExtension<EthereumNetwork> for EthereumMetaExtension {
403  fn from_block(block: EthereumBlockNumber) -> Self {
404    Self { block }
405  }
406}
407
408#[crate::_impls(keep)]
409#[async_graphql::Object]
410impl EthereumMetaExtension {
411  /// Information about a specific block. The hash of the block
412  /// will be null if the _meta field has a block constraint that asks for
413  /// a block number. It will be filled if the _meta field has no block constraint
414  /// and therefore asks for the latest  block
415  pub async fn block(
416    &self,
417    ctx: &async_graphql::Context<'_>,
418  ) -> async_graphql::Result<EthereumMetaBlock> {
419    let hash_and_timestamp =
420      ctx.look_ahead().field("hash").exists() || ctx.look_ahead().field("timestamp").exists();
421
422    if !hash_and_timestamp {
423      return Ok(EthereumMetaBlock {
424        number: self.block,
425        hash: H256::default(),
426        timestamp: 0,
427      });
428    }
429
430    ctx
431      .data::<Arc<EthereumNetwork>>()?
432      .block_info_lookup(self.block.into())
433      .await
434      .map(|info| EthereumMetaBlock {
435        number: self.block,
436        hash: info.block_hash,
437        timestamp: info.timestamp.timestamp() as u64,
438      })
439      .map_err(Into::into)
440  }
441}
442
443#[test]
444fn test_block_id() {
445  use async_graphql::{InputType, value};
446  let number = EthereumBlockId::parse(Some(value!(123))).unwrap();
447  assert_eq!(
448    number,
449    EthereumBlockId::Number(EthereumBlockNumber::new(123))
450  );
451
452  let number = EthereumBlockId::parse(Some(value!({ "number": 123 }))).unwrap();
453  assert_eq!(
454    number,
455    EthereumBlockId::Number(EthereumBlockNumber::new(123))
456  );
457
458  let hash = EthereumBlockId::parse(Some(value!(
459    "0x0101010101010101010101010101010101010101010101010101010101010101"
460  )))
461  .unwrap();
462  assert_eq!(
463    hash,
464    EthereumBlockId::Hash(
465      H256::from_str("0x0101010101010101010101010101010101010101010101010101010101010101").unwrap()
466    )
467  );
468
469  let hash = EthereumBlockId::parse(Some(
470    value!({ "hash": "0x0101010101010101010101010101010101010101010101010101010101010101" }),
471  ))
472  .unwrap();
473  assert_eq!(
474    hash,
475    EthereumBlockId::Hash(
476      H256::from_str("0x0101010101010101010101010101010101010101010101010101010101010101").unwrap()
477    )
478  );
479}