1use std::sync::{Arc, Mutex};
20
21use solana_client::nonblocking::rpc_client::RpcClient;
22use solana_sdk::commitment_config::CommitmentConfig;
23use solana_sdk::pubkey::Pubkey;
24use solana_sdk::signature::{Keypair, Signature, Signer};
25use solana_sdk::transaction::Transaction;
26use spl_associated_token_account::get_associated_token_address;
27
28use scemadex_sdk::{
29 Bond, BondOutcome, Clock, Result, ScemaDexError, SettlementConfig, SettlementMachine,
30 SlashDistribution, SystemClock, Usdc,
31};
32
33use crate::{DEVNET_RPC, USDC_DECIMALS};
34
35#[derive(Clone, Copy, Debug)]
38pub struct Beneficiaries {
39 pub caller: Pubkey,
41 pub challengers: Pubkey,
43 pub insurance: Pubkey,
45 pub lineage: Pubkey,
47}
48
49#[derive(Clone, Copy, Debug)]
51pub struct SlashTransfer {
52 pub destination: Pubkey,
53 pub amount: Usdc,
54 pub signature: Signature,
55}
56
57pub struct OptimisticUsdcSettler<C: Clock = SystemClock> {
60 machine: SettlementMachine<C>,
61 rpc: RpcClient,
62 agent: Arc<Keypair>,
63 usdc_mint: Pubkey,
64 beneficiaries: Beneficiaries,
65 last_transfers: Mutex<Vec<SlashTransfer>>,
66}
67
68impl OptimisticUsdcSettler<SystemClock> {
69 pub fn devnet(
71 agent: Arc<Keypair>,
72 usdc_mint: Pubkey,
73 beneficiaries: Beneficiaries,
74 config: SettlementConfig,
75 ) -> Self {
76 Self::new(DEVNET_RPC, agent, usdc_mint, beneficiaries, config, SystemClock)
77 }
78}
79
80impl<C: Clock> OptimisticUsdcSettler<C> {
81 pub fn new(
83 rpc_url: impl Into<String>,
84 agent: Arc<Keypair>,
85 usdc_mint: Pubkey,
86 beneficiaries: Beneficiaries,
87 config: SettlementConfig,
88 clock: C,
89 ) -> Self {
90 Self {
91 machine: SettlementMachine::new(config, clock),
92 rpc: RpcClient::new_with_commitment(rpc_url.into(), CommitmentConfig::confirmed()),
93 agent,
94 usdc_mint,
95 beneficiaries,
96 last_transfers: Mutex::new(Vec::new()),
97 }
98 }
99
100 pub fn machine(&self) -> &SettlementMachine<C> {
103 &self.machine
104 }
105
106 pub fn agent_usdc_account(&self) -> Pubkey {
108 get_associated_token_address(&self.agent.pubkey(), &self.usdc_mint)
109 }
110
111 pub fn last_transfers(&self) -> Vec<SlashTransfer> {
113 self.last_transfers.lock().map(|t| t.clone()).unwrap_or_default()
114 }
115
116 pub fn open(&self, bond: &Bond) -> Result<()> {
118 self.machine.open(bond)
119 }
120
121 pub fn provision(&self, digest: &str, outcome: BondOutcome) -> Result<()> {
123 self.machine.provision(digest, outcome).map(|_| ())
124 }
125
126 pub async fn finalize_and_settle(&self, digest: &str) -> Result<(BondOutcome, Vec<SlashTransfer>)> {
130 let outcome = self.machine.finalize(digest)?;
131 self.disburse(digest, outcome).await
132 }
133
134 pub async fn sweep_and_settle(&self) -> Result<Vec<(String, BondOutcome)>> {
138 let matured = self.machine.sweep()?;
139 let mut out = Vec::with_capacity(matured.len());
140 for (digest, outcome) in matured {
141 self.disburse(&digest, outcome).await?;
142 out.push((digest, outcome));
143 }
144 Ok(out)
145 }
146
147 async fn disburse(&self, digest: &str, outcome: BondOutcome) -> Result<(BondOutcome, Vec<SlashTransfer>)> {
150 let transfers = match outcome {
151 BondOutcome::Honored => Vec::new(),
152 BondOutcome::Slashed => {
153 let dist = self
154 .machine
155 .slash_distribution(digest)
156 .ok_or_else(|| ScemaDexError::Bond(format!("no slash distribution for {digest}")))?;
157 self.settle_split(dist).await?
158 }
159 };
160 if let Ok(mut slot) = self.last_transfers.lock() {
161 slot.clone_from(&transfers);
162 }
163 Ok((outcome, transfers))
164 }
165
166 async fn settle_split(&self, dist: SlashDistribution) -> Result<Vec<SlashTransfer>> {
168 let legs = [
169 (self.beneficiaries.caller, dist.caller),
170 (self.beneficiaries.challengers, dist.challengers),
171 (self.beneficiaries.insurance, dist.insurance),
172 (self.beneficiaries.lineage, dist.lineage),
173 ];
174 let mut transfers = Vec::new();
175 for (destination, amount) in legs {
176 if amount.0 == 0 {
177 continue;
178 }
179 let signature = self.transfer(destination, amount).await?;
180 transfers.push(SlashTransfer { destination, amount, signature });
181 tracing::info!(
182 %destination,
183 amount_micro_usdc = amount.0,
184 %signature,
185 "slash slice transferred"
186 );
187 }
188 Ok(transfers)
189 }
190
191 async fn transfer(&self, destination: Pubkey, amount: Usdc) -> Result<Signature> {
194 let source = self.agent_usdc_account();
195 let ix = spl_token::instruction::transfer_checked(
196 &spl_token::id(),
197 &source,
198 &self.usdc_mint,
199 &destination,
200 &self.agent.pubkey(),
201 &[],
202 amount.0,
203 USDC_DECIMALS,
204 )
205 .map_err(|e| ScemaDexError::Bond(format!("build transfer ix: {e}")))?;
206
207 let blockhash = self
208 .rpc
209 .get_latest_blockhash()
210 .await
211 .map_err(|e| ScemaDexError::Bond(format!("get blockhash: {e}")))?;
212 let tx = Transaction::new_signed_with_payer(
213 &[ix],
214 Some(&self.agent.pubkey()),
215 &[self.agent.as_ref()],
216 blockhash,
217 );
218 self.rpc
219 .send_and_confirm_transaction(&tx)
220 .await
221 .map_err(|e| ScemaDexError::Bond(format!("submit slash transfer: {e}")))
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use scemadex_sdk::{ManualClock, SlashRouting};
229 use std::str::FromStr;
230
231 fn beneficiaries() -> Beneficiaries {
232 Beneficiaries {
233 caller: Pubkey::new_unique(),
234 challengers: Pubkey::new_unique(),
235 insurance: Pubkey::new_unique(),
236 lineage: Pubkey::new_unique(),
237 }
238 }
239
240 fn settler(config: SettlementConfig) -> OptimisticUsdcSettler<ManualClock> {
241 OptimisticUsdcSettler::new(
242 DEVNET_RPC,
243 Arc::new(Keypair::new()),
244 Pubkey::from_str("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU").unwrap(),
245 beneficiaries(),
246 config,
247 ManualClock::new(1_000),
248 )
249 }
250
251 fn bond(digest: &str, amount: u64) -> Bond {
252 Bond {
253 intent_digest: digest.into(),
254 amount: Usdc(amount),
255 min_out_raw: 1_000_000,
256 deadline_unix: 0,
257 }
258 }
259
260 #[tokio::test]
261 async fn honored_bond_moves_nothing_offline() {
262 let s = settler(SettlementConfig::optimistic(60));
265 s.open(&bond("h", 1_000)).unwrap();
266 s.provision("h", BondOutcome::Honored).unwrap();
267 s.machine().clock().advance(61);
268 let (outcome, transfers) = s.finalize_and_settle("h").await.unwrap();
269 assert_eq!(outcome, BondOutcome::Honored);
270 assert!(transfers.is_empty(), "an honored bond disburses nothing");
271 }
272
273 #[test]
274 fn window_gates_finalization() {
275 let s = settler(SettlementConfig::optimistic(60));
277 s.open(&bond("d", 1_000)).unwrap();
278 s.provision("d", BondOutcome::Honored).unwrap();
279 assert!(s.machine().sweep().unwrap().is_empty(), "window still open");
280 }
281
282 #[test]
283 fn slash_distribution_uses_the_configured_routing() {
284 let routing = SlashRouting {
287 to_caller_bps: 4_000,
288 to_challengers_bps: 3_000,
289 to_insurance_bps: 2_000,
290 to_lineage_bps: 1_000,
291 };
292 let s = settler(SettlementConfig::optimistic(0).with_slash_routing(routing));
293 s.open(&bond("s", 1_003)).unwrap();
294 s.provision("s", BondOutcome::Slashed).unwrap();
296 let d = s.machine().slash_distribution("s").unwrap();
297 assert_eq!(d.caller.0 + d.challengers.0 + d.insurance.0 + d.lineage.0, 1_003);
298 assert_eq!(d.challengers, Usdc(300));
299 assert_eq!(d.insurance, Usdc(200));
300 assert_eq!(d.lineage, Usdc(100));
301 assert_eq!(d.caller, Usdc(403), "caller absorbs the dust");
302 }
303}