sui_gql_client/queries/
gas_payment.rs

1use af_sui_types::{Address as SuiAddress, ObjectRef, Version};
2use cynic::GraphQlResponse;
3
4use super::fragments::PageInfoForward;
5use crate::scalars::{BigInt, Digest};
6use crate::{GraphQlClient, GraphQlErrors, GraphQlResponseExt as _, schema};
7
8#[derive(thiserror::Error, Debug)]
9pub enum Error<E> {
10    #[error("Missing data in initial response")]
11    MissingInitialData,
12    #[error("Not enough SUI coins found for budget {budget}")]
13    NotEnoughSui { budget: u64 },
14    #[error("Missing data in page response")]
15    PageMissingData,
16    #[error("Missing coin balance data")]
17    MissingCoinBalance,
18    #[error("Missing coin digest data")]
19    MissingCoinDigest,
20    #[error(transparent)]
21    Client(E),
22    #[error(transparent)]
23    Server(#[from] GraphQlErrors),
24}
25
26pub(super) async fn query<C: GraphQlClient>(
27    client: &C,
28    sponsor: SuiAddress,
29    budget: u64,
30    exclude: Vec<SuiAddress>,
31) -> Result<Vec<ObjectRef>, Error<C::Error>> {
32    let mut vars = Variables {
33        address: sponsor,
34        first: None,
35        after: None,
36    };
37    let query: Query = client
38        .query(vars.clone())
39        .await
40        .map_err(Error::Client)?
41        .try_into_data()
42        .map_err(Error::Server)?
43        .ok_or(Error::MissingInitialData)?;
44
45    let Query {
46        address: Some(Address {
47            coins: mut connection,
48        }),
49    } = query
50    else {
51        return Err(Error::MissingInitialData);
52    };
53
54    let mut coins = vec![];
55    let mut balance = 0;
56    loop {
57        let CoinConnection { nodes, page_info } = connection;
58        for Coin {
59            object_id,
60            version,
61            digest,
62            coin_balance,
63        } in nodes
64            .into_iter()
65            .filter(|n| !exclude.contains(&n.object_id))
66        {
67            let digest = digest.ok_or(Error::MissingCoinDigest)?;
68            let coin_balance = coin_balance.ok_or(Error::MissingCoinBalance)?.into_inner();
69            coins.push((object_id, version, digest.0));
70            balance += coin_balance;
71            if balance >= budget {
72                return Ok(coins);
73            }
74        }
75
76        if !page_info.has_next_page {
77            break;
78        }
79        vars.after = page_info.end_cursor;
80        connection = {
81            let response: GraphQlResponse<Query> =
82                client.query(vars.clone()).await.map_err(Error::Client)?;
83            let Some(Query {
84                address: Some(Address { coins }),
85            }) = response.try_into_data()?
86            else {
87                return Err(Error::PageMissingData);
88            };
89            coins
90        };
91    }
92
93    Err(Error::NotEnoughSui { budget })
94}
95
96#[cfg(test)]
97#[allow(clippy::unwrap_used)]
98#[test]
99fn gql_output() {
100    use cynic::QueryBuilder as _;
101
102    let vars = Variables {
103        address: SuiAddress::new(rand::random()),
104        first: None,
105        after: None,
106    };
107    let operation = Query::build(vars);
108    insta::assert_snapshot!(operation.query, @r###"
109    query Query($address: SuiAddress!, $first: Int, $after: String) {
110      address(address: $address) {
111        coins(type: "0x2::sui::SUI", first: $first, after: $after) {
112          nodes {
113            address
114            version
115            digest
116            coinBalance
117          }
118          pageInfo {
119            hasNextPage
120            endCursor
121          }
122        }
123      }
124    }
125    "###);
126}
127
128// =============================================================================
129//  Inner query fragments
130// =============================================================================
131
132#[derive(cynic::QueryVariables, Clone, Debug)]
133struct Variables {
134    address: SuiAddress,
135    first: Option<i32>,
136    after: Option<String>,
137}
138
139#[derive(cynic::QueryFragment, Clone, Debug)]
140#[cynic(variables = "Variables")]
141struct Query {
142    #[arguments(address: $address)]
143    address: Option<Address>,
144}
145
146#[derive(cynic::QueryFragment, Clone, Debug)]
147#[cynic(variables = "Variables")]
148struct Address {
149    #[arguments(type: "0x2::sui::SUI", first: $first, after: $after)]
150    coins: CoinConnection,
151}
152
153#[derive(cynic::QueryFragment, Clone, Debug)]
154struct CoinConnection {
155    nodes: Vec<Coin>,
156    page_info: PageInfoForward,
157}
158
159#[derive(cynic::QueryFragment, Clone, Debug)]
160struct Coin {
161    #[cynic(rename = "address")]
162    object_id: SuiAddress,
163    version: Version,
164    digest: Option<Digest>,
165    coin_balance: Option<BigInt<u64>>,
166}