sui-gql-client 0.26.2

Sui GraphQL RPC client by Aftermath using `cynic`
Documentation
use af_move_type::MoveInstance;
use af_move_type::otw::Otw;
use af_sui_types::{Address, ObjectRef};
use futures::Stream;
use sui_framework_sdk::coin::Coin;
use sui_gql_schema::scalars::Base64Bcs;

use super::model::fragments::{ObjectFilter, PageInfo};
use super::stream;
use crate::queries::Error;
use crate::queries::model::fragments::{Checkpoint, ObjectConnection};
use crate::{GraphQlClient, GraphQlResponseExt, schema};

#[derive(cynic::QueryVariables, Clone, Debug)]
struct Variables {
    filter: ObjectFilter,
    after: Option<String>,
    first: Option<i32>,
}

#[derive(cynic::QueryFragment, Clone, Debug)]
#[cynic(variables = "Variables")]
struct Query {
    checkpoint: Option<Checkpoint>,
    #[arguments(filter: $filter, first: $first, after: $after)]
    objects: Option<ObjectConnection>,
}

pub(super) fn query<C: GraphQlClient>(
    client: &C,
    owner: Address,
    type_: Option<String>,
    page_size: Option<u32>,
) -> impl Stream<Item = super::Result<(u64, ObjectRef, u64), C>> {
    let coin_type = if let Some(_ct) = &type_ {
        type_
    } else {
        Some("0x2::coin::Coin".to_string())
    };

    let filter = ObjectFilter {
        owner: Some(owner),
        type_: coin_type,
        owner_kind: None,
    };
    let vars = Variables {
        after: None,
        first: page_size.map(|v| v.try_into().unwrap_or(i32::MAX)),
        filter,
    };
    stream::forward(client, vars, request)
}

async fn request<C: GraphQlClient>(
    client: &C,
    vars: Variables,
) -> super::Result<
    stream::Page<impl Iterator<Item = super::Result<(u64, ObjectRef, u64), C>> + use<C>>,
    C,
> {
    let data = client
        .query::<Query, _>(vars)
        .await
        .map_err(Error::Client)?
        .try_into_data()?;

    let stream::Page { info, data } = extract(data)?;
    Ok(stream::Page::new(
        info,
        data.map(|r| r.map_err(Error::MissingData)),
    ))
}

#[expect(clippy::type_complexity)]
fn extract(
    data: Option<Query>,
) -> Result<stream::Page<impl Iterator<Item = Result<(u64, ObjectRef, u64), String>>>, &'static str>
{
    graphql_extract::extract!(data => {
        checkpoint?
        objects? {
            nodes[] {
                id
                object
            }
            page_info
        }
    });

    let nodes = nodes
        .into_iter()
        .map(move |r: Result<_, &'static str>| -> Result<_, String> {
            let (id, bcs) = r?;
            let obj = bcs
                .ok_or_else(|| format!("BCS for object {id}"))
                .map(Base64Bcs::into_inner)?;

            let coin_obj = obj
                .as_struct()
                .ok_or_else(|| format!("Object {id} is not a struct"))?;
            let coin_obj: MoveInstance<Coin<Otw>> =
                MoveInstance::from_raw_struct(coin_obj.object_type().clone(), coin_obj.contents())
                    .map_err(|e| format!("Error from raw struct {e}"))?;
            let obj_ref = (obj.object_id(), obj.version(), obj.digest());
            Ok((
                checkpoint.sequence_number,
                obj_ref,
                coin_obj.value.balance.value,
            ))
        });
    Ok(stream::Page::new(page_info, nodes))
}

impl stream::UpdatePageInfo for Variables {
    fn update_page_info(&mut self, info: &PageInfo) {
        self.after.clone_from(&info.end_cursor)
    }
}

#[cfg(test)]
#[test]
fn gql_output() -> color_eyre::Result<()> {
    use cynic::QueryBuilder as _;

    let vars = Variables {
        filter: ObjectFilter {
            type_: None,
            owner: None,
            owner_kind: None,
        },
        after: None,
        first: None,
    };

    let operation = Query::build(vars);
    insta::assert_snapshot!(operation.query, @r"
    query Query($filter: ObjectFilter!, $after: String, $first: Int) {
      checkpoint {
        sequenceNumber
      }
      objects(filter: $filter, first: $first, after: $after) {
        nodes {
          address
          objectBcs
        }
        pageInfo {
          hasNextPage
          endCursor
        }
      }
    }
    ");
    Ok(())
}