use std::sync::{Arc, Mutex};
use async_trait::async_trait;
use solana_client::nonblocking::rpc_client::RpcClient;
use solana_sdk::commitment_config::CommitmentConfig;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::{Keypair, Signature, Signer};
use solana_sdk::transaction::Transaction;
use spl_associated_token_account::get_associated_token_address;
use scemadex_sdk::{
Bond, BondConfig, BondEngine, BondLedger, BondOutcome, Conviction, EscrowBondEngine, Fill,
Result, ScemaDexError, Solution, Usdc,
};
pub const DEVNET_RPC: &str = "https://api.devnet.solana.com";
pub const USDC_DECIMALS: u8 = 6;
pub struct DevnetUsdcSettler {
inner: EscrowBondEngine,
rpc: RpcClient,
agent: Arc<Keypair>,
usdc_mint: Pubkey,
beneficiary_token_account: Pubkey,
last_signature: Mutex<Option<Signature>>,
}
impl DevnetUsdcSettler {
pub fn new(
rpc_url: impl Into<String>,
agent: Arc<Keypair>,
usdc_mint: Pubkey,
beneficiary_token_account: Pubkey,
config: BondConfig,
) -> Self {
Self {
inner: EscrowBondEngine::new(config),
rpc: RpcClient::new_with_commitment(rpc_url.into(), CommitmentConfig::confirmed()),
agent,
usdc_mint,
beneficiary_token_account,
last_signature: Mutex::new(None),
}
}
pub fn devnet(
agent: Arc<Keypair>,
usdc_mint: Pubkey,
beneficiary_token_account: Pubkey,
) -> Self {
Self::new(
DEVNET_RPC,
agent,
usdc_mint,
beneficiary_token_account,
BondConfig::default(),
)
}
pub fn agent_usdc_account(&self) -> Pubkey {
get_associated_token_address(&self.agent.pubkey(), &self.usdc_mint)
}
pub fn quote_fee(&self, conviction: Conviction) -> Usdc {
self.inner.quote_fee(conviction)
}
pub fn ledger(&self) -> BondLedger {
self.inner.ledger()
}
pub fn open_bonds(&self) -> usize {
self.inner.open_bonds()
}
pub fn last_signature(&self) -> Option<Signature> {
self.last_signature.lock().ok().and_then(|s| *s)
}
pub async fn settle_onchain(
&self,
bond: &Bond,
fill: &Fill,
) -> Result<(BondOutcome, Option<Signature>)> {
let outcome = self.inner.settle(bond, fill).await?;
let sig = match outcome {
BondOutcome::Slashed => Some(self.transfer_bond(bond.amount).await?),
BondOutcome::Honored => None,
};
if let Some(sig) = sig {
if let Ok(mut slot) = self.last_signature.lock() {
*slot = Some(sig);
}
tracing::info!(
amount_micro_usdc = bond.amount.0,
signature = %sig,
"bond slashed — devnet USDC transferred to caller"
);
}
Ok((outcome, sig))
}
async fn transfer_bond(&self, amount: Usdc) -> Result<Signature> {
let source = self.agent_usdc_account();
let ix = spl_token::instruction::transfer_checked(
&spl_token::id(),
&source,
&self.usdc_mint,
&self.beneficiary_token_account,
&self.agent.pubkey(),
&[],
amount.0,
USDC_DECIMALS,
)
.map_err(|e| ScemaDexError::Bond(format!("build transfer ix: {e}")))?;
let blockhash = self
.rpc
.get_latest_blockhash()
.await
.map_err(|e| ScemaDexError::Bond(format!("get blockhash: {e}")))?;
let tx = Transaction::new_signed_with_payer(
&[ix],
Some(&self.agent.pubkey()),
&[self.agent.as_ref()],
blockhash,
);
self.rpc
.send_and_confirm_transaction(&tx)
.await
.map_err(|e| ScemaDexError::Bond(format!("submit slash transfer: {e}")))
}
}
#[async_trait]
impl BondEngine for DevnetUsdcSettler {
async fn escrow(&self, solution: &Solution) -> Result<Bond> {
self.inner.escrow(solution).await
}
async fn settle(&self, bond: &Bond, fill: &Fill) -> Result<BondOutcome> {
self.settle_onchain(bond, fill).await.map(|(o, _)| o)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
fn settler() -> DevnetUsdcSettler {
DevnetUsdcSettler::devnet(
Arc::new(Keypair::new()),
Pubkey::from_str("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU").unwrap(),
Pubkey::new_unique(),
)
}
#[test]
fn agent_usdc_account_is_deterministic() {
let s = settler();
assert_eq!(s.agent_usdc_account(), s.agent_usdc_account());
}
#[tokio::test]
async fn honored_settlement_moves_nothing_offline() {
use scemadex_sdk::RoutePolicy;
let s = settler();
let sol = scemadex_sdk::ReferenceRoutePolicy
.solve(&scemadex_sdk::demo_intent())
.await
.unwrap();
let bond = s.escrow(&sol).await.unwrap();
let fill = Fill {
amount_out: scemadex_sdk::Amount::new(bond.min_out_raw, USDC_DECIMALS),
executed_unix: 0,
};
let (outcome, sig) = s.settle_onchain(&bond, &fill).await.unwrap();
assert_eq!(outcome, BondOutcome::Honored);
assert!(sig.is_none(), "honored settlement must not touch the chain");
}
}