sui_rpc/client/
coin_selection.rs

1use super::Client;
2use super::Result;
3use crate::field::FieldMaskUtil;
4use crate::proto::sui::rpc::v2::ListOwnedObjectsRequest;
5use crate::proto::sui::rpc::v2::Object;
6use futures::StreamExt;
7use prost_types::FieldMask;
8use std::str::FromStr;
9use sui_sdk_types::Address;
10use sui_sdk_types::StructTag;
11use sui_sdk_types::TypeTag;
12
13impl Client {
14    /// Selects coins of a specific type owned by an address until the total value meets the required amount.
15    ///
16    /// # Arguments
17    /// * `owner_address` - The address that owns the coins
18    /// * `coin_type` - The TypeTag of coins to select
19    /// * `amount` - The minimum total amount needed
20    /// * `exclude` - Array of addresses to exclude from selection
21    ///
22    /// # Returns
23    /// A vector of `Object` instances representing the selected coins
24    ///
25    /// # Errors
26    /// Returns an error if there are insufficient funds to meet the required amount or if there is an RPC error
27    pub async fn select_coins(
28        &self,
29        owner_address: &Address,
30        coin_type: &TypeTag,
31        amount: u64,
32        exclude: &[Address],
33    ) -> Result<Vec<Object>> {
34        let coin_struct = StructTag::coin(coin_type.clone());
35        let list_request = ListOwnedObjectsRequest::default()
36            .with_owner(owner_address)
37            .with_object_type(&coin_struct)
38            .with_page_size(500u32)
39            .with_read_mask(FieldMask::from_paths([
40                "object_id",
41                "version",
42                "digest",
43                "balance",
44                "owner",
45            ]));
46
47        let mut coin_stream = Box::pin(self.list_owned_objects(list_request));
48        let mut selected_coins = Vec::new();
49        let mut total = 0u64;
50
51        while let Some(object_result) = coin_stream.next().await {
52            let object = object_result?;
53
54            if Address::from_str(object.object_id()).is_ok_and(|addr| exclude.contains(&addr)) {
55                continue;
56            }
57
58            total = total.saturating_add(object.balance());
59            selected_coins.push(object);
60
61            if total >= amount {
62                return Ok(selected_coins);
63            }
64        }
65
66        Err(tonic::Status::failed_precondition(format!(
67            "Insufficient funds for address [{owner_address}], requested amount: {amount}, total available: {total}"
68        )))
69    }
70
71    /// Selects up to N coins of a specific type owned by an address.
72    ///
73    /// # Arguments
74    /// * `owner_address` - The address that owns the coins
75    /// * `coin_type` - The TypeTag of coins to select
76    /// * `n` - The maximum number of coins to select
77    /// * `exclude` - Array of addresses to exclude from selection
78    ///
79    /// # Returns
80    /// A vector of `Object` instances representing the selected coins (may be fewer than `n` if not enough coins are available)
81    ///
82    /// # Errors
83    /// Returns an error if there is an RPC error during coin retrieval
84    pub async fn select_up_to_n_largest_coins(
85        &self,
86        owner_address: &Address,
87        coin_type: &TypeTag,
88        n: usize,
89        exclude: &[Address],
90    ) -> Result<Vec<Object>> {
91        let mut selected_coins = vec![];
92
93        let coin_struct = StructTag::coin(coin_type.clone());
94        let list_request = ListOwnedObjectsRequest::default()
95            .with_owner(owner_address)
96            .with_object_type(&coin_struct)
97            .with_page_size(500u32)
98            .with_read_mask(FieldMask::from_paths([
99                "object_id",
100                "version",
101                "digest",
102                "balance",
103                "owner",
104            ]));
105
106        let mut coin_stream = Box::pin(self.list_owned_objects(list_request));
107
108        while selected_coins.len() < n {
109            match coin_stream.next().await {
110                Some(Ok(object)) => {
111                    if !Address::from_str(object.object_id())
112                        .is_ok_and(|addr| exclude.contains(&addr))
113                    {
114                        selected_coins.push(object);
115                    }
116                }
117                Some(Err(e)) => return Err(e),
118                None => break,
119            }
120        }
121
122        Ok(selected_coins)
123    }
124}