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 loop {
106 if start.elapsed() > timeout {
107 anyhow::bail!(
108 "Transaction executed but checkpoint wait timed out (waited {:.0}s)",
109 timeout.as_secs()
110 );
111 }
112
113 let mut poll_req = GetTransactionRequest::default();
114 poll_req.digest = Some(digest_str.clone());
115 poll_req.read_mask = Some(FieldMask::from_paths(vec!["digest", "checkpoint"]));
116
117 match client.ledger_client().get_transaction(poll_req).await {
118 Ok(resp) => {
119 if resp
120 .get_ref()
121 .transaction
122 .as_ref()
123 .and_then(|t| t.checkpoint)
124 .is_some()
125 {
126 return Ok(digest_str);
127 }
128 }
129 Err(_) => {
130 }
132 }
133
134 tokio::time::sleep(Duration::from_millis(1000)).await;
135 }
136}
137
138pub async fn get_usdc_balance(
141 rpc_url: &str,
142 address: &Address,
143) -> anyhow::Result<(u64, u64)> {
144 let mut client = sui_rpc::Client::new(rpc_url)
145 .map_err(|e| anyhow::anyhow!("Failed to create Sui RPC client: {e}"))?;
146
147 let mut request = GetBalanceRequest::default();
148 request.owner = Some(address.to_string());
149 request.coin_type = Some(USDC_COIN_TYPE.to_string());
150
151 let response = client
152 .state_client()
153 .get_balance(request)
154 .await
155 .map_err(|e| anyhow::anyhow!("Failed to get USDC balance: {e}"))?
156 .into_inner();
157
158 match response.balance {
159 Some(b) => Ok((
160 b.address_balance.unwrap_or(0),
161 b.coin_balance.unwrap_or(0),
162 )),
163 None => Ok((0, 0)),
164 }
165}
166
167pub async fn consolidate_usdc_coins(
170 rpc_url: &str,
171 keypair: &Ed25519PrivateKey,
172 sender: &Address,
173 coin_bal: u64,
174) -> anyhow::Result<String> {
175 let mut client = sui_rpc::Client::new(rpc_url)
176 .map_err(|e| anyhow::anyhow!("Failed to create Sui RPC client: {e}"))?;
177
178 let usdc_type = TypeTag::from_str(USDC_COIN_TYPE)?;
179
180 let usdc_coins = client
181 .select_coins(sender, &usdc_type, coin_bal, &[])
182 .await
183 .map_err(|e| anyhow::anyhow!("Failed to find USDC coins: {e}"))?;
184
185 if usdc_coins.is_empty() {
186 return Ok("no USDC coins to consolidate".into());
187 }
188
189 let mut tx = TransactionBuilder::new();
190
191 for usdc_coin in &usdc_coins {
192 let obj_id = Address::from_str(usdc_coin.object_id())?;
193 let digest = Digest::from_str(usdc_coin.digest())?;
194 let coin_arg = tx.object(ObjectInput::owned(obj_id, usdc_coin.version(), digest));
195 let self_arg = tx.pure(sender);
196 tx.move_call(
197 Function::new(
198 Address::TWO,
199 sui_sdk_types::Identifier::from_static("coin"),
200 sui_sdk_types::Identifier::from_static("send_funds"),
201 )
202 .with_type_args(vec![usdc_type.clone()]),
203 vec![coin_arg, self_arg],
204 );
205 }
206
207 tx.set_sender(*sender);
208 tx.set_gas_budget(0);
209
210 let transaction = tx
211 .build(&mut client)
212 .await
213 .map_err(|e| anyhow::anyhow!("Failed to build consolidation transaction: {e}"))?;
214
215 let signature = keypair
216 .sign_transaction(&transaction)
217 .map_err(|e| anyhow::anyhow!("Signing failed: {e}"))?;
218
219 let request = ExecuteTransactionRequest::new(transaction.into())
220 .with_signatures(vec![signature.into()])
221 .with_read_mask(FieldMask::from_paths(vec!["digest"]));
222
223 let digest_str = execute_and_wait_for_checkpoint(
224 &mut client,
225 request,
226 Duration::from_secs(60),
227 )
228 .await?;
229
230 Ok(digest_str)
231}