iota_sdk/apis/coin_read.rs
1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{future, sync::Arc};
6
7use futures::{StreamExt, stream};
8use futures_core::Stream;
9use iota_json_rpc_api::CoinReadApiClient;
10use iota_json_rpc_types::{Balance, Coin, CoinPage, IotaCirculatingSupply, IotaCoinMetadata};
11use iota_types::{
12 balance::Supply,
13 base_types::{IotaAddress, ObjectID},
14};
15
16use crate::{
17 RpcClient,
18 error::{Error, IotaRpcResult},
19};
20
21/// Defines methods that retrieve information from the IOTA network regarding
22/// the coins owned by an address.
23#[derive(Debug, Clone)]
24pub struct CoinReadApi {
25 api: Arc<RpcClient>,
26}
27
28impl CoinReadApi {
29 pub(crate) fn new(api: Arc<RpcClient>) -> Self {
30 Self { api }
31 }
32
33 /// Get coins for the given address filtered by coin type.
34 /// Results are paginated.
35 ///
36 /// The coin type defaults to `0x2::iota::IOTA`.
37 ///
38 /// # Examples
39 ///
40 /// ```rust,no_run
41 /// use std::str::FromStr;
42 ///
43 /// use iota_sdk::IotaClientBuilder;
44 /// use iota_types::base_types::IotaAddress;
45 ///
46 /// #[tokio::main]
47 /// async fn main() -> Result<(), anyhow::Error> {
48 /// let iota = IotaClientBuilder::default().build_testnet().await?;
49 /// let address = IotaAddress::from_str("0x0000....0000")?;
50 /// let coin_type = String::from("0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC");
51 /// let coins = iota
52 /// .coin_read_api()
53 /// .get_coins(address, coin_type, None, None)
54 /// .await?;
55 /// Ok(())
56 /// }
57 /// ```
58 pub async fn get_coins(
59 &self,
60 owner: IotaAddress,
61 coin_type: impl Into<Option<String>>,
62 cursor: impl Into<Option<ObjectID>>,
63 limit: impl Into<Option<usize>>,
64 ) -> IotaRpcResult<CoinPage> {
65 Ok(self
66 .api
67 .http
68 .get_coins(owner, coin_type.into(), cursor.into(), limit.into())
69 .await?)
70 }
71
72 /// Get all the coins for the given address regardless of coin type.
73 /// Results are paginated.
74 ///
75 /// # Examples
76 ///
77 /// ```rust,no_run
78 /// use std::str::FromStr;
79 ///
80 /// use iota_sdk::IotaClientBuilder;
81 /// use iota_types::base_types::IotaAddress;
82 ///
83 /// #[tokio::main]
84 /// async fn main() -> Result<(), anyhow::Error> {
85 /// let iota = IotaClientBuilder::default().build_testnet().await?;
86 /// let address = IotaAddress::from_str("0x0000....0000")?;
87 /// let coins = iota
88 /// .coin_read_api()
89 /// .get_all_coins(address, None, None)
90 /// .await?;
91 /// Ok(())
92 /// }
93 /// ```
94 pub async fn get_all_coins(
95 &self,
96 owner: IotaAddress,
97 cursor: impl Into<Option<ObjectID>>,
98 limit: impl Into<Option<usize>>,
99 ) -> IotaRpcResult<CoinPage> {
100 Ok(self
101 .api
102 .http
103 .get_all_coins(owner, cursor.into(), limit.into())
104 .await?)
105 }
106
107 /// Get the coins for the given address filtered by coin type.
108 /// Returns a stream.
109 ///
110 /// The coin type defaults to `0x2::iota::IOTA`.
111 ///
112 /// # Examples
113 ///
114 /// ```rust,no_run
115 /// use std::str::FromStr;
116 ///
117 /// use iota_sdk::IotaClientBuilder;
118 /// use iota_types::base_types::IotaAddress;
119 ///
120 /// #[tokio::main]
121 /// async fn main() -> Result<(), anyhow::Error> {
122 /// let iota = IotaClientBuilder::default().build_testnet().await?;
123 /// let address = IotaAddress::from_str("0x0000....0000")?;
124 /// let coin_type = String::from("0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC");
125 /// let coins = iota.coin_read_api().get_coins_stream(address, coin_type);
126 /// Ok(())
127 /// }
128 /// ```
129 pub fn get_coins_stream(
130 &self,
131 owner: IotaAddress,
132 coin_type: impl Into<Option<String>>,
133 ) -> impl Stream<Item = Coin> + '_ {
134 let coin_type = coin_type.into();
135
136 stream::unfold(
137 (
138 vec![],
139 // cursor
140 None,
141 // has_next_page
142 true,
143 coin_type,
144 ),
145 move |(mut data, cursor, has_next_page, coin_type)| async move {
146 if let Some(item) = data.pop() {
147 Some((item, (data, cursor, /* has_next_page */ true, coin_type)))
148 } else if has_next_page {
149 let page = self
150 .get_coins(owner, coin_type.clone(), cursor, Some(100))
151 .await
152 .ok()?;
153 let mut data = page.data;
154 data.reverse();
155 data.pop().map(|item| {
156 (
157 item,
158 (data, page.next_cursor, page.has_next_page, coin_type),
159 )
160 })
161 } else {
162 None
163 }
164 },
165 )
166 }
167
168 /// Get a list of coins for the given address filtered by coin type with at
169 /// least `amount` total value.
170 ///
171 /// If it is not possible to select enough coins, this function will return
172 /// an [`Error::InsufficientFunds`].
173 ///
174 /// The coin type defaults to `0x2::iota::IOTA`.
175 ///
176 /// # Examples
177 ///
178 /// ```rust,no_run
179 /// use std::str::FromStr;
180 ///
181 /// use iota_sdk::IotaClientBuilder;
182 /// use iota_types::base_types::IotaAddress;
183 ///
184 /// #[tokio::main]
185 /// async fn main() -> Result<(), anyhow::Error> {
186 /// let iota = IotaClientBuilder::default().build_testnet().await?;
187 /// let address = IotaAddress::from_str("0x0000....0000")?;
188 /// let coin_type = String::from("0x168da5bf1f48dafc111b0a488fa454aca95e0b5e::usdc::USDC");
189 /// let coins = iota
190 /// .coin_read_api()
191 /// .select_coins(address, coin_type, 5, vec![])
192 /// .await?;
193 /// Ok(())
194 /// }
195 /// ```
196 pub async fn select_coins(
197 &self,
198 address: IotaAddress,
199 coin_type: impl Into<Option<String>>,
200 amount: u128,
201 exclude: Vec<ObjectID>,
202 ) -> IotaRpcResult<Vec<Coin>> {
203 let mut total = 0u128;
204 let coins = self
205 .get_coins_stream(address, coin_type.into())
206 .filter(|coin: &Coin| future::ready(!exclude.contains(&coin.coin_object_id)))
207 .take_while(|coin: &Coin| {
208 let ready = future::ready(total < amount);
209 total += coin.balance as u128;
210 ready
211 })
212 .collect::<Vec<_>>()
213 .await;
214
215 if total < amount {
216 return Err(Error::InsufficientFunds { address, amount });
217 }
218 Ok(coins)
219 }
220
221 /// Get the balance for the given address filtered by coin type.
222 ///
223 /// The coin type defaults to `0x2::iota::IOTA`.
224 ///
225 /// # Examples
226 ///
227 /// ```rust,no_run
228 /// use std::str::FromStr;
229 ///
230 /// use iota_sdk::IotaClientBuilder;
231 /// use iota_types::base_types::IotaAddress;
232 ///
233 /// #[tokio::main]
234 /// async fn main() -> Result<(), anyhow::Error> {
235 /// let iota = IotaClientBuilder::default().build_testnet().await?;
236 /// let address = IotaAddress::from_str("0x0000....0000")?;
237 /// let balance = iota.coin_read_api().get_balance(address, None).await?;
238 /// Ok(())
239 /// }
240 /// ```
241 pub async fn get_balance(
242 &self,
243 owner: IotaAddress,
244 coin_type: impl Into<Option<String>>,
245 ) -> IotaRpcResult<Balance> {
246 Ok(self.api.http.get_balance(owner, coin_type.into()).await?)
247 }
248
249 /// Get a list of balances grouped by coin type and owned by the given
250 /// address.
251 ///
252 /// # Examples
253 ///
254 /// ```rust,no_run
255 /// use std::str::FromStr;
256 ///
257 /// use iota_sdk::IotaClientBuilder;
258 /// use iota_types::base_types::IotaAddress;
259 ///
260 /// #[tokio::main]
261 /// async fn main() -> Result<(), anyhow::Error> {
262 /// let iota = IotaClientBuilder::default().build_testnet().await?;
263 /// let address = IotaAddress::from_str("0x0000....0000")?;
264 /// let all_balances = iota.coin_read_api().get_all_balances(address).await?;
265 /// Ok(())
266 /// }
267 /// ```
268 pub async fn get_all_balances(&self, owner: IotaAddress) -> IotaRpcResult<Vec<Balance>> {
269 Ok(self.api.http.get_all_balances(owner).await?)
270 }
271
272 /// Get the coin metadata (name, symbol, description, decimals, etc.) for a
273 /// given coin type.
274 ///
275 /// # Examples
276 ///
277 /// ```rust,no_run
278 /// use iota_sdk::IotaClientBuilder;
279 /// #[tokio::main]
280 /// async fn main() -> Result<(), anyhow::Error> {
281 /// let iota = IotaClientBuilder::default().build_testnet().await?;
282 /// let coin_metadata = iota
283 /// .coin_read_api()
284 /// .get_coin_metadata("0x2::iota::IOTA")
285 /// .await?;
286 /// Ok(())
287 /// }
288 /// ```
289 pub async fn get_coin_metadata(
290 &self,
291 coin_type: impl Into<String>,
292 ) -> IotaRpcResult<Option<IotaCoinMetadata>> {
293 Ok(self.api.http.get_coin_metadata(coin_type.into()).await?)
294 }
295
296 /// Get the total supply for a given coin type.
297 ///
298 /// # Examples
299 ///
300 /// ```rust,no_run
301 /// use iota_sdk::IotaClientBuilder;
302 ///
303 /// #[tokio::main]
304 /// async fn main() -> Result<(), anyhow::Error> {
305 /// let iota = IotaClientBuilder::default().build_testnet().await?;
306 /// let total_supply = iota
307 /// .coin_read_api()
308 /// .get_total_supply("0x2::iota::IOTA")
309 /// .await?;
310 /// Ok(())
311 /// }
312 /// ```
313 pub async fn get_total_supply(&self, coin_type: impl Into<String>) -> IotaRpcResult<Supply> {
314 Ok(self.api.http.get_total_supply(coin_type.into()).await?)
315 }
316
317 /// Get the IOTA circulating supply summary.
318 ///
319 /// # Examples
320 ///
321 /// ```rust,no_run
322 /// use iota_sdk::IotaClientBuilder;
323 ///
324 /// #[tokio::main]
325 /// async fn main() -> Result<(), anyhow::Error> {
326 /// let iota = IotaClientBuilder::default().build_testnet().await?;
327 /// let circulating_supply = iota.coin_read_api().get_circulating_supply().await?;
328 /// Ok(())
329 /// }
330 /// ```
331 pub async fn get_circulating_supply(&self) -> IotaRpcResult<IotaCirculatingSupply> {
332 Ok(self.api.http.get_circulating_supply().await?)
333 }
334}