sui_gql_client/queries/
latest_version_at_checkpoint_v2.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
use af_sui_types::{ObjectId, Version};
use cynic::{QueryFragment, QueryVariables};

use crate::{schema, GraphQlClient, GraphQlErrors, GraphQlResponseExt as _};

#[derive(thiserror::Error, Clone, Debug, PartialEq)]
pub enum Error<C: std::error::Error> {
    #[error(
        "The latest checkpoint number ({0}) known to the server is smaller than the queried \
        checkpoint"
    )]
    WatermarkTooLow(u64),
    #[error("Missing latest checkpoint number")]
    NoLatestCheckpoint,
    #[error("No data in GraphQL response")]
    NoData,
    #[error("No transaction blocks found for object {id}")]
    NoTransactionBlocks { id: ObjectId },
    #[error("Missing transaction effects")]
    MissingTxEffects,
    #[error("In client: {0}")]
    Client(C),
    #[error("From server: {0}")]
    Server(#[from] GraphQlErrors),
}

/// Get the object's highest version in the network history up to a checkpoint number.
///
/// IMPORTANT: this query uses the transactions table to retrieve the version of the
/// requested object. In case the GQL server is not storing the transactions table, this
/// query is not going to provide the expected results.
///
/// # Arguments
/// - `client`: GQL client implementation
/// - `id`: ID of the object
/// - `ckpt_num`: highest checkpoint to consider; the server will scan the history `<= ckpt_num`
pub async fn query<C: GraphQlClient>(
    client: &C,
    id: ObjectId,
    ckpt_num: u64,
) -> Result<u64, Error<C::Error>> {
    let Some(mut data): Option<Query> = client
        .query(Variables {
            object_id: Some(id),
            checkpoint_num: Some(ckpt_num + 1),
        })
        .await
        .map_err(Error::Client)?
        .try_into_data()?
    else {
        return Err(Error::NoData);
    };

    let watermark = data
        .checkpoint
        .ok_or(Error::NoLatestCheckpoint)?
        .sequence_number;
    if watermark < ckpt_num {
        return Err(Error::WatermarkTooLow(watermark));
    }

    Ok(data
        .transaction_blocks
        .nodes
        .pop()
        .ok_or_else(|| Error::NoTransactionBlocks { id })?
        .effects
        .ok_or(Error::MissingTxEffects)?
        .lamport_version)
}

#[derive(QueryVariables, Debug)]
struct Variables {
    checkpoint_num: Option<Version>,
    object_id: Option<ObjectId>,
}

#[derive(QueryFragment, Debug)]
#[cynic(graphql_type = "Query", variables = "Variables")]
struct Query {
    checkpoint: Option<CheckpointNumber>,

    #[arguments(last: 1, filter: { beforeCheckpoint: $checkpoint_num, changedObject: $object_id })]
    transaction_blocks: TransactionBlockConnection,
}

#[derive(QueryFragment, Debug)]
#[cynic(graphql_type = "Checkpoint")]
struct CheckpointNumber {
    sequence_number: Version,
}

#[derive(QueryFragment, Debug)]
struct TransactionBlockConnection {
    nodes: Vec<TransactionBlock>,
}

#[derive(QueryFragment, Debug)]
struct TransactionBlock {
    effects: Option<TransactionBlockEffects>,
}

#[derive(QueryFragment, Debug)]
struct TransactionBlockEffects {
    lamport_version: Version,
}

#[cfg(test)]
#[allow(clippy::unwrap_used)]
#[test]
fn init_gql_output() {
    use cynic::QueryBuilder as _;
    let vars = Variables {
        object_id: Some(
            "0x4264c07a42f9d002c1244e43a1f0fa21c49e4a25c7202c597b8476ef6bb57113"
                .parse()
                .unwrap(),
        ),
        checkpoint_num: Some(54773328),
    };
    let operation = Query::build(vars);
    insta::assert_snapshot!(operation.query, @r###"
    query Query($checkpointNum: UInt53, $objectId: SuiAddress) {
      checkpoint {
        sequenceNumber
      }
      transactionBlocks(last: 1, filter: {beforeCheckpoint: $checkpointNum, changedObject: $objectId}) {
        nodes {
          effects {
            lamportVersion
          }
        }
      }
    }
    "###);
}