sui_gql_client/queries/
gas_payment.rs1use 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#[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}