1use std::sync::{Arc, Mutex};
43
44use async_trait::async_trait;
45use solana_client::nonblocking::rpc_client::RpcClient;
46use solana_sdk::commitment_config::CommitmentConfig;
47use solana_sdk::pubkey::Pubkey;
48use solana_sdk::signature::{Keypair, Signature, Signer};
49use solana_sdk::transaction::Transaction;
50use spl_associated_token_account::get_associated_token_address;
51
52use scemadex_sdk::{
53 Bond, BondConfig, BondEngine, BondLedger, BondOutcome, Conviction, EscrowBondEngine, Fill,
54 Result, ScemaDexError, Solution, Usdc,
55};
56
57pub const DEVNET_RPC: &str = "https://api.devnet.solana.com";
59
60pub const USDC_DECIMALS: u8 = 6;
62
63pub struct DevnetUsdcSettler {
66 inner: EscrowBondEngine,
67 rpc: RpcClient,
68 agent: Arc<Keypair>,
69 usdc_mint: Pubkey,
70 beneficiary_token_account: Pubkey,
72 last_signature: Mutex<Option<Signature>>,
73}
74
75impl DevnetUsdcSettler {
76 pub fn new(
78 rpc_url: impl Into<String>,
79 agent: Arc<Keypair>,
80 usdc_mint: Pubkey,
81 beneficiary_token_account: Pubkey,
82 config: BondConfig,
83 ) -> Self {
84 Self {
85 inner: EscrowBondEngine::new(config),
86 rpc: RpcClient::new_with_commitment(rpc_url.into(), CommitmentConfig::confirmed()),
87 agent,
88 usdc_mint,
89 beneficiary_token_account,
90 last_signature: Mutex::new(None),
91 }
92 }
93
94 pub fn devnet(
96 agent: Arc<Keypair>,
97 usdc_mint: Pubkey,
98 beneficiary_token_account: Pubkey,
99 ) -> Self {
100 Self::new(
101 DEVNET_RPC,
102 agent,
103 usdc_mint,
104 beneficiary_token_account,
105 BondConfig::default(),
106 )
107 }
108
109 pub fn agent_usdc_account(&self) -> Pubkey {
111 get_associated_token_address(&self.agent.pubkey(), &self.usdc_mint)
112 }
113
114 pub fn quote_fee(&self, conviction: Conviction) -> Usdc {
116 self.inner.quote_fee(conviction)
117 }
118
119 pub fn ledger(&self) -> BondLedger {
121 self.inner.ledger()
122 }
123
124 pub fn open_bonds(&self) -> usize {
126 self.inner.open_bonds()
127 }
128
129 pub fn last_signature(&self) -> Option<Signature> {
131 self.last_signature.lock().ok().and_then(|s| *s)
132 }
133
134 pub async fn settle_onchain(
137 &self,
138 bond: &Bond,
139 fill: &Fill,
140 ) -> Result<(BondOutcome, Option<Signature>)> {
141 let outcome = self.inner.settle(bond, fill).await?;
142 let sig = match outcome {
143 BondOutcome::Slashed => Some(self.transfer_bond(bond.amount).await?),
144 BondOutcome::Honored => None,
145 };
146 if let Some(sig) = sig {
147 if let Ok(mut slot) = self.last_signature.lock() {
148 *slot = Some(sig);
149 }
150 tracing::info!(
151 amount_micro_usdc = bond.amount.0,
152 signature = %sig,
153 "bond slashed — devnet USDC transferred to caller"
154 );
155 }
156 Ok((outcome, sig))
157 }
158
159 async fn transfer_bond(&self, amount: Usdc) -> Result<Signature> {
162 let source = self.agent_usdc_account();
163 let ix = spl_token::instruction::transfer_checked(
164 &spl_token::id(),
165 &source,
166 &self.usdc_mint,
167 &self.beneficiary_token_account,
168 &self.agent.pubkey(),
169 &[],
170 amount.0,
171 USDC_DECIMALS,
172 )
173 .map_err(|e| ScemaDexError::Bond(format!("build transfer ix: {e}")))?;
174
175 let blockhash = self
176 .rpc
177 .get_latest_blockhash()
178 .await
179 .map_err(|e| ScemaDexError::Bond(format!("get blockhash: {e}")))?;
180 let tx = Transaction::new_signed_with_payer(
181 &[ix],
182 Some(&self.agent.pubkey()),
183 &[self.agent.as_ref()],
184 blockhash,
185 );
186 self.rpc
187 .send_and_confirm_transaction(&tx)
188 .await
189 .map_err(|e| ScemaDexError::Bond(format!("submit slash transfer: {e}")))
190 }
191}
192
193#[async_trait]
194impl BondEngine for DevnetUsdcSettler {
195 async fn escrow(&self, solution: &Solution) -> Result<Bond> {
196 self.inner.escrow(solution).await
197 }
198
199 async fn settle(&self, bond: &Bond, fill: &Fill) -> Result<BondOutcome> {
202 self.settle_onchain(bond, fill).await.map(|(o, _)| o)
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209 use std::str::FromStr;
210
211 fn settler() -> DevnetUsdcSettler {
212 DevnetUsdcSettler::devnet(
213 Arc::new(Keypair::new()),
214 Pubkey::from_str("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU").unwrap(),
215 Pubkey::new_unique(),
216 )
217 }
218
219 #[test]
220 fn agent_usdc_account_is_deterministic() {
221 let s = settler();
222 assert_eq!(s.agent_usdc_account(), s.agent_usdc_account());
223 }
224
225 #[tokio::test]
226 async fn honored_settlement_moves_nothing_offline() {
227 use scemadex_sdk::RoutePolicy;
228 let s = settler();
230 let sol = scemadex_sdk::ReferenceRoutePolicy
231 .solve(&scemadex_sdk::demo_intent())
232 .await
233 .unwrap();
234 let bond = s.escrow(&sol).await.unwrap();
235 let fill = Fill {
236 amount_out: scemadex_sdk::Amount::new(bond.min_out_raw, USDC_DECIMALS),
237 executed_unix: 0,
238 };
239 let (outcome, sig) = s.settle_onchain(&bond, &fill).await.unwrap();
240 assert_eq!(outcome, BondOutcome::Honored);
241 assert!(sig.is_none(), "honored settlement must not touch the chain");
242 }
243}