sui_rpc/client/v2/
coin_selection.rs

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