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