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