iota_sdk_graphql_client/api/
coins.rs1use cynic::QueryBuilder;
8use futures::Stream;
9use iota_types::{Address, Identifier, StructTag, framework::Coin};
10
11use crate::{
12 GraphQLClient, ListObjectsQuery,
13 api::define_query,
14 error::GraphQLResult,
15 pagination::{Page, PaginationFilter},
16 query_types::{CoinMetadata, CoinMetadataArgs, CoinMetadataQueryFragment, ObjectFilter},
17 streams::stream_paginated_query,
18};
19
20define_query! {
21 #[derive(Clone)]
23 pub struct ListCoinsQuery {
24 client: GraphQLClient,
25 owner: Address,
26 coin_type: Option<StructTag>,
27 pagination: PaginationFilter,
28 }
29 output: GraphQLResult<Page<Coin>>;
30}
31
32impl ListCoinsQuery {
33 pub fn coin_type(mut self, coin_type: StructTag) -> Self {
35 self.coin_type = Some(coin_type);
36 self
37 }
38
39 pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
41 self.pagination = pagination;
42 self
43 }
44
45 pub fn stream(self) -> impl Stream<Item = GraphQLResult<Coin>> + Unpin {
48 let pagination = self.pagination.clone();
49 stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
50 }
51
52 fn objects_query(self) -> ListObjectsQuery {
53 let type_tag = self.coin_type.map(StructTag::new_coin).unwrap_or_else(|| {
54 StructTag::new(
55 Address::FRAMEWORK,
56 Identifier::from_static("coin"),
57 Identifier::from_static("Coin"),
58 Default::default(),
59 )
60 });
61 ListObjectsQuery::new(self.client)
62 .filter(ObjectFilter {
63 type_tag: Some(type_tag.to_string()),
64 owner: Some(self.owner),
65 object_ids: None,
66 })
67 .pagination(self.pagination)
68 }
69
70 async fn send(self) -> GraphQLResult<Page<Coin>> {
71 let response = self.objects_query().await?;
72
73 Ok(Page::new(
74 response.page_info,
75 response
76 .data
77 .iter()
78 .flat_map(Coin::try_from_object)
79 .collect::<Vec<_>>(),
80 ))
81 }
82}
83
84define_query! {
85 #[derive(Clone)]
87 pub struct ListGasCoinsQuery {
88 coins: ListCoinsQuery,
89 }
90 output: GraphQLResult<Page<Coin>>;
91}
92
93impl ListGasCoinsQuery {
94 pub fn pagination(mut self, pagination: PaginationFilter) -> Self {
96 self.coins = self.coins.pagination(pagination);
97 self
98 }
99
100 pub fn stream(self) -> impl Stream<Item = GraphQLResult<Coin>> + Unpin {
103 let pagination = self.coins.pagination.clone();
104 stream_paginated_query(move |page| self.clone().pagination(page).send(), pagination)
105 }
106
107 async fn send(self) -> GraphQLResult<Page<Coin>> {
108 self.coins.send().await
109 }
110}
111
112define_query! {
113 pub struct GetCoinMetadataQuery {
116 client: GraphQLClient,
117 coin_type: StructTag,
118 }
119 output: GraphQLResult<Option<CoinMetadata>>;
120}
121
122impl GetCoinMetadataQuery {
123 async fn send(self) -> GraphQLResult<Option<CoinMetadata>> {
124 let coin_type = self.coin_type.to_string();
125 let operation = CoinMetadataQueryFragment::build(CoinMetadataArgs {
126 coin_type: &coin_type,
127 });
128 let response = self.client.run_query(&operation).await?;
129
130 Ok(response.coin_metadata)
131 }
132}
133
134define_query! {
135 pub struct GetTotalSupplyQuery {
137 client: GraphQLClient,
138 coin_type: StructTag,
139 }
140 output: GraphQLResult<Option<u64>>;
141}
142
143impl GetTotalSupplyQuery {
144 async fn send(self) -> GraphQLResult<Option<u64>> {
145 let coin_metadata = self.client.coin_metadata(self.coin_type).await?;
146
147 coin_metadata
148 .and_then(|c| c.supply)
149 .map(|c| c.try_into())
150 .transpose()
151 }
152}
153
154impl GraphQLClient {
155 pub fn coins(&self, owner: Address) -> ListCoinsQuery {
158 ListCoinsQuery {
159 client: self.clone(),
160 owner,
161 coin_type: None,
162 pagination: PaginationFilter::default(),
163 }
164 }
165
166 pub fn gas_coins(&self, owner: Address) -> ListGasCoinsQuery {
168 ListGasCoinsQuery {
169 coins: self.coins(owner).coin_type(StructTag::new_gas()),
170 }
171 }
172
173 pub fn coin_metadata(&self, coin_type: StructTag) -> GetCoinMetadataQuery {
175 GetCoinMetadataQuery {
176 client: self.clone(),
177 coin_type,
178 }
179 }
180
181 pub fn total_supply(&self, coin_type: StructTag) -> GetTotalSupplyQuery {
183 GetTotalSupplyQuery {
184 client: self.clone(),
185 coin_type,
186 }
187 }
188}
189
190#[cfg(all(test, not(target_arch = "wasm32")))]
191mod tests {
192 use futures::StreamExt;
193 use iota_types::{Address, Ed25519PublicKey, StructTag};
194 use tokio::time;
195
196 use crate::{
197 client::LOCAL_HOST,
198 faucet::FaucetClient,
199 test_utils::{
200 NUM_COINS_FROM_FAUCET, assert_backward_page, assert_forward_page, backward_page,
201 forward_page, sent_variables, test_client,
202 },
203 };
204
205 #[tokio::test]
206 async fn coin_metadata_and_total_supply_send_the_coin_type() {
207 let vars = sent_variables("CoinMetadataQueryFragment", |client| async move {
208 let _ = client.coin_metadata(StructTag::new_gas()).await;
209 })
210 .await;
211 assert_eq!(vars["coinType"], "0x2::iota::IOTA");
212
213 let vars = sent_variables("CoinMetadataQueryFragment", |client| async move {
214 let _ = client.total_supply(StructTag::new_gas()).await;
215 })
216 .await;
217 assert_eq!(vars["coinType"], "0x2::iota::IOTA");
218 }
219
220 #[tokio::test]
221 async fn coins_sends_the_owner_coin_type_and_pagination() {
222 let vars = sent_variables("ObjectsQueryFragment", |client| async move {
223 let _ = client
224 .coins(Address::STD)
225 .coin_type(StructTag::new_gas())
226 .pagination(backward_page())
227 .await;
228 })
229 .await;
230 assert_eq!(vars["filter"]["owner"], Address::STD.to_string());
231 assert_eq!(
232 vars["filter"]["type"],
233 StructTag::new_coin(StructTag::new_gas()).to_string()
234 );
235 assert_backward_page(&vars);
236
237 let vars = sent_variables("ObjectsQueryFragment", |client| async move {
238 let _ = client
239 .coins(Address::STD)
240 .coin_type(StructTag::new_gas())
241 .pagination(forward_page())
242 .await;
243 })
244 .await;
245 assert_forward_page(&vars);
246 }
247
248 #[tokio::test]
249 async fn gas_coins_sends_the_gas_coin_type() {
250 let vars = sent_variables("ObjectsQueryFragment", |client| async move {
251 let _ = client
252 .gas_coins(Address::STD)
253 .pagination(backward_page())
254 .await;
255 })
256 .await;
257 assert_eq!(
258 vars["filter"]["type"],
259 StructTag::new_coin(StructTag::new_gas()).to_string()
260 );
261 }
262
263 #[tokio::test]
264 async fn coins_default_to_every_coin_type() {
265 let vars = sent_variables("ObjectsQueryFragment", |client| async move {
266 let _ = client.coins(Address::STD).pagination(backward_page()).await;
267 })
268 .await;
269 assert_eq!(vars["filter"]["type"], "0x2::coin::Coin");
270 }
271
272 #[tokio::test]
273 async fn test_coins_query() {
274 let client = test_client();
275 client
276 .coins(Address::STD)
277 .await
278 .map_err(|e| {
279 format!(
280 "Coins query failed for {} network: Error {e}",
281 client.rpc_server()
282 )
283 })
284 .unwrap();
285 }
286
287 #[tokio::test]
288 async fn test_coins_stream() {
289 let client = test_client();
290 let faucet = match client.rpc_server().as_str() {
291 LOCAL_HOST => FaucetClient::new_localnet(),
292 _ => return,
296 };
297 let key = Ed25519PublicKey::random();
298 let address = key.derive_address();
299 faucet
300 .request_and_wait_for_finalized(address, &client)
301 .await
302 .unwrap();
303
304 const MAX_RETRIES: u32 = 10;
305 const RETRY_DELAY: time::Duration = time::Duration::from_secs(1);
306
307 let mut num_coins = 0;
311 for attempt in 0..MAX_RETRIES {
312 num_coins = 0;
313 let mut stream = client.coins(address).stream();
314 let mut errored = false;
315 while let Some(result) = stream.next().await {
316 match result {
317 Ok(_) => num_coins += 1,
318 Err(_) => {
319 errored = true;
320 break;
321 }
322 }
323 }
324
325 if !errored && num_coins >= NUM_COINS_FROM_FAUCET {
326 break;
327 }
328 if attempt < MAX_RETRIES - 1 {
329 time::sleep(RETRY_DELAY).await;
330 }
331 }
332
333 assert!(
334 num_coins >= NUM_COINS_FROM_FAUCET,
335 "expected at least {NUM_COINS_FROM_FAUCET} coins for {address}, got {num_coins}"
336 );
337 }
338
339 #[tokio::test]
340 async fn test_coin_metadata_query() {
341 let client = test_client();
342 client
343 .coin_metadata(StructTag::new_gas())
344 .await
345 .map_err(|e| {
346 format!(
347 "Coin metadata query failed for {} network: Error: {e}",
348 client.rpc_server()
349 )
350 })
351 .unwrap()
352 .unwrap();
353 }
354
355 #[tokio::test]
356 async fn test_total_supply() {
357 let client = test_client();
358 client
359 .total_supply(StructTag::new_gas())
360 .await
361 .map_err(|e| {
362 format!(
363 "Total supply query failed for {} network. Error: {e}",
364 client.rpc_server()
365 )
366 })
367 .unwrap()
368 .unwrap();
369 }
370}