1use std::str::FromStr;
2use std::time::Duration;
3use sui_crypto::ed25519::Ed25519PrivateKey;
4use sui_crypto::SuiSigner;
5use sui_rpc::field::FieldMask;
6use sui_rpc::field::FieldMaskUtil;
7use sui_rpc::proto::sui::rpc::v2::{ExecuteTransactionRequest, GetBalanceRequest, GetTransactionRequest};
8use sui_sdk_types::{Address, Digest, TypeTag};
9use sui_transaction_builder::{Function, ObjectInput, TransactionBuilder};
10
11const USDC_COIN_TYPE: &str =
12 "0xdba34672e30cb065b1f93e3ab55318768fd6fef66c15942c9f7cb846e2f900e7::usdc::USDC";
13const MIN_PAYMENT_USDC: u64 = 10_000; pub async fn send_payment(
18 rpc_url: &str,
19 keypair: &Ed25519PrivateKey,
20 sender: &Address,
21 platform_address: &Address,
22) -> anyhow::Result<String> {
23 let (addr_bal, coin_bal) = get_usdc_balance(rpc_url, sender).await?;
25 if addr_bal + coin_bal < MIN_PAYMENT_USDC {
26 anyhow::bail!("Insufficient USDC balance (need at least 0.01 USDC)");
27 }
28
29 if coin_bal > 0 {
31 consolidate_usdc_coins(rpc_url, keypair, sender, coin_bal).await?;
32 }
33
34 let mut client = sui_rpc::Client::new(rpc_url)
35 .map_err(|e| anyhow::anyhow!("Failed to create Sui RPC client: {e}"))?;
36
37 let usdc_type = TypeTag::from_str(USDC_COIN_TYPE)?;
38
39 let mut tx = TransactionBuilder::new();
41
42 let balance_arg = tx.funds_withdrawal_balance(usdc_type.clone(), MIN_PAYMENT_USDC);
43 let recipient_arg = tx.pure(platform_address);
44 tx.move_call(
45 Function::new(
46 Address::TWO,
47 sui_sdk_types::Identifier::from_static("balance"),
48 sui_sdk_types::Identifier::from_static("send_funds"),
49 )
50 .with_type_args(vec![usdc_type.clone()]),
51 vec![balance_arg, recipient_arg],
52 );
53
54 tx.set_sender(*sender);
55 tx.set_gas_budget(0);
56
57 let transaction = tx
59 .build(&mut client)
60 .await
61 .map_err(|e| anyhow::anyhow!("Failed to build transaction: {e}"))?;
62
63 let signature = keypair
64 .sign_transaction(&transaction)
65 .map_err(|e| anyhow::anyhow!("Signing failed: {e}"))?;
66
67 let request = ExecuteTransactionRequest::new(transaction.into())
68 .with_signatures(vec![signature.into()])
69 .with_read_mask(FieldMask::from_paths(vec!["digest"]));
70
71 let digest_str = execute_and_wait_for_checkpoint(
72 &mut client,
73 request,
74 Duration::from_secs(60),
75 )
76 .await?;
77
78 Ok(digest_str)
79}
80
81async fn execute_and_wait_for_checkpoint(
84 client: &mut sui_rpc::Client,
85 request: ExecuteTransactionRequest,
86 timeout: Duration,
87) -> anyhow::Result<String> {
88 let digest_str = {
89 let proto_tx = request
90 .transaction
91 .as_ref()
92 .ok_or_else(|| anyhow::anyhow!("Missing transaction in execution request"))?;
93 let sdk_tx = sui_sdk_types::Transaction::try_from(proto_tx)
94 .map_err(|e| anyhow::anyhow!("Failed to compute transaction digest: {e}"))?;
95 sdk_tx.digest().to_string()
96 };
97
98 client
99 .execution_client()
100 .execute_transaction(request)
101 .await
102 .map_err(|e| anyhow::anyhow!("Transaction execution failed: {e}"))?;
103
104 let start = std::time::Instant::now();
105 let mut delay = Duration::from_secs(1);
106 let max_delay = Duration::from_secs(8);
107 loop {
108 if start.elapsed() > timeout {
109 anyhow::bail!(
110 "Transaction executed but checkpoint wait timed out (waited {:.0}s)",
111 timeout.as_secs()
112 );
113 }
114
115 tokio::time::sleep(delay.min(timeout.saturating_sub(start.elapsed()))).await;
116 delay = (delay * 2).min(max_delay);
117
118 let mut poll_req = GetTransactionRequest::default();
119 poll_req.digest = Some(digest_str.clone());
120 poll_req.read_mask = Some(FieldMask::from_paths(vec!["digest", "checkpoint"]));
121
122 match client.ledger_client().get_transaction(poll_req).await {
123 Ok(resp) => {
124 if resp
125 .get_ref()
126 .transaction
127 .as_ref()
128 .and_then(|t| t.checkpoint)
129 .is_some()
130 {
131 return Ok(digest_str);
132 }
133 }
134 Err(_) => {
135 }
137 }
138 }
139}
140
141pub async fn get_usdc_balance(
144 rpc_url: &str,
145 address: &Address,
146) -> anyhow::Result<(u64, u64)> {
147 let mut client = sui_rpc::Client::new(rpc_url)
148 .map_err(|e| anyhow::anyhow!("Failed to create Sui RPC client: {e}"))?;
149
150 let mut request = GetBalanceRequest::default();
151 request.owner = Some(address.to_string());
152 request.coin_type = Some(USDC_COIN_TYPE.to_string());
153
154 let response = client
155 .state_client()
156 .get_balance(request)
157 .await
158 .map_err(|e| anyhow::anyhow!("Failed to get USDC balance: {e}"))?
159 .into_inner();
160
161 match response.balance {
162 Some(b) => Ok((
163 b.address_balance.unwrap_or(0),
164 b.coin_balance.unwrap_or(0),
165 )),
166 None => Ok((0, 0)),
167 }
168}
169
170pub async fn consolidate_usdc_coins(
173 rpc_url: &str,
174 keypair: &Ed25519PrivateKey,
175 sender: &Address,
176 coin_bal: u64,
177) -> anyhow::Result<String> {
178 let mut client = sui_rpc::Client::new(rpc_url)
179 .map_err(|e| anyhow::anyhow!("Failed to create Sui RPC client: {e}"))?;
180
181 let usdc_type = TypeTag::from_str(USDC_COIN_TYPE)?;
182
183 let usdc_coins = client
184 .select_coins(sender, &usdc_type, coin_bal, &[])
185 .await
186 .map_err(|e| anyhow::anyhow!("Failed to find USDC coins: {e}"))?;
187
188 if usdc_coins.is_empty() {
189 return Ok("no USDC coins to consolidate".into());
190 }
191
192 let mut tx = TransactionBuilder::new();
193
194 for usdc_coin in &usdc_coins {
195 let obj_id = Address::from_str(usdc_coin.object_id())?;
196 let digest = Digest::from_str(usdc_coin.digest())?;
197 let coin_arg = tx.object(ObjectInput::owned(obj_id, usdc_coin.version(), digest));
198 let self_arg = tx.pure(sender);
199 tx.move_call(
200 Function::new(
201 Address::TWO,
202 sui_sdk_types::Identifier::from_static("coin"),
203 sui_sdk_types::Identifier::from_static("send_funds"),
204 )
205 .with_type_args(vec![usdc_type.clone()]),
206 vec![coin_arg, self_arg],
207 );
208 }
209
210 tx.set_sender(*sender);
211 tx.set_gas_budget(0);
212
213 let transaction = tx
214 .build(&mut client)
215 .await
216 .map_err(|e| anyhow::anyhow!("Failed to build consolidation transaction: {e}"))?;
217
218 let signature = keypair
219 .sign_transaction(&transaction)
220 .map_err(|e| anyhow::anyhow!("Signing failed: {e}"))?;
221
222 let request = ExecuteTransactionRequest::new(transaction.into())
223 .with_signatures(vec![signature.into()])
224 .with_read_mask(FieldMask::from_paths(vec!["digest"]));
225
226 let digest_str = execute_and_wait_for_checkpoint(
227 &mut client,
228 request,
229 Duration::from_secs(60),
230 )
231 .await?;
232
233 Ok(digest_str)
234}