use std::sync::{Arc, Mutex};
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, BondOutcome, Clock, Result, ScemaDexError, SettlementConfig, SettlementMachine,
SlashDistribution, SystemClock, Usdc,
};
use crate::{DEVNET_RPC, USDC_DECIMALS};
#[derive(Clone, Copy, Debug)]
pub struct Beneficiaries {
pub caller: Pubkey,
pub challengers: Pubkey,
pub insurance: Pubkey,
pub lineage: Pubkey,
}
#[derive(Clone, Copy, Debug)]
pub struct SlashTransfer {
pub destination: Pubkey,
pub amount: Usdc,
pub signature: Signature,
}
pub struct OptimisticUsdcSettler<C: Clock = SystemClock> {
machine: SettlementMachine<C>,
rpc: RpcClient,
agent: Arc<Keypair>,
usdc_mint: Pubkey,
beneficiaries: Beneficiaries,
last_transfers: Mutex<Vec<SlashTransfer>>,
}
impl OptimisticUsdcSettler<SystemClock> {
pub fn devnet(
agent: Arc<Keypair>,
usdc_mint: Pubkey,
beneficiaries: Beneficiaries,
config: SettlementConfig,
) -> Self {
Self::new(DEVNET_RPC, agent, usdc_mint, beneficiaries, config, SystemClock)
}
}
impl<C: Clock> OptimisticUsdcSettler<C> {
pub fn new(
rpc_url: impl Into<String>,
agent: Arc<Keypair>,
usdc_mint: Pubkey,
beneficiaries: Beneficiaries,
config: SettlementConfig,
clock: C,
) -> Self {
Self {
machine: SettlementMachine::new(config, clock),
rpc: RpcClient::new_with_commitment(rpc_url.into(), CommitmentConfig::confirmed()),
agent,
usdc_mint,
beneficiaries,
last_transfers: Mutex::new(Vec::new()),
}
}
pub fn machine(&self) -> &SettlementMachine<C> {
&self.machine
}
pub fn agent_usdc_account(&self) -> Pubkey {
get_associated_token_address(&self.agent.pubkey(), &self.usdc_mint)
}
pub fn last_transfers(&self) -> Vec<SlashTransfer> {
self.last_transfers.lock().map(|t| t.clone()).unwrap_or_default()
}
pub fn open(&self, bond: &Bond) -> Result<()> {
self.machine.open(bond)
}
pub fn provision(&self, digest: &str, outcome: BondOutcome) -> Result<()> {
self.machine.provision(digest, outcome).map(|_| ())
}
pub async fn finalize_and_settle(&self, digest: &str) -> Result<(BondOutcome, Vec<SlashTransfer>)> {
let outcome = self.machine.finalize(digest)?;
self.disburse(digest, outcome).await
}
pub async fn sweep_and_settle(&self) -> Result<Vec<(String, BondOutcome)>> {
let matured = self.machine.sweep()?;
let mut out = Vec::with_capacity(matured.len());
for (digest, outcome) in matured {
self.disburse(&digest, outcome).await?;
out.push((digest, outcome));
}
Ok(out)
}
async fn disburse(&self, digest: &str, outcome: BondOutcome) -> Result<(BondOutcome, Vec<SlashTransfer>)> {
let transfers = match outcome {
BondOutcome::Honored => Vec::new(),
BondOutcome::Slashed => {
let dist = self
.machine
.slash_distribution(digest)
.ok_or_else(|| ScemaDexError::Bond(format!("no slash distribution for {digest}")))?;
self.settle_split(dist).await?
}
};
if let Ok(mut slot) = self.last_transfers.lock() {
slot.clone_from(&transfers);
}
Ok((outcome, transfers))
}
async fn settle_split(&self, dist: SlashDistribution) -> Result<Vec<SlashTransfer>> {
let legs = [
(self.beneficiaries.caller, dist.caller),
(self.beneficiaries.challengers, dist.challengers),
(self.beneficiaries.insurance, dist.insurance),
(self.beneficiaries.lineage, dist.lineage),
];
let mut transfers = Vec::new();
for (destination, amount) in legs {
if amount.0 == 0 {
continue;
}
let signature = self.transfer(destination, amount).await?;
transfers.push(SlashTransfer { destination, amount, signature });
tracing::info!(
%destination,
amount_micro_usdc = amount.0,
%signature,
"slash slice transferred"
);
}
Ok(transfers)
}
async fn transfer(&self, destination: Pubkey, amount: Usdc) -> Result<Signature> {
let source = self.agent_usdc_account();
let ix = spl_token::instruction::transfer_checked(
&spl_token::id(),
&source,
&self.usdc_mint,
&destination,
&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}")))
}
}
#[cfg(test)]
mod tests {
use super::*;
use scemadex_sdk::{ManualClock, SlashRouting};
use std::str::FromStr;
fn beneficiaries() -> Beneficiaries {
Beneficiaries {
caller: Pubkey::new_unique(),
challengers: Pubkey::new_unique(),
insurance: Pubkey::new_unique(),
lineage: Pubkey::new_unique(),
}
}
fn settler(config: SettlementConfig) -> OptimisticUsdcSettler<ManualClock> {
OptimisticUsdcSettler::new(
DEVNET_RPC,
Arc::new(Keypair::new()),
Pubkey::from_str("4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU").unwrap(),
beneficiaries(),
config,
ManualClock::new(1_000),
)
}
fn bond(digest: &str, amount: u64) -> Bond {
Bond {
intent_digest: digest.into(),
amount: Usdc(amount),
min_out_raw: 1_000_000,
deadline_unix: 0,
}
}
#[tokio::test]
async fn honored_bond_moves_nothing_offline() {
let s = settler(SettlementConfig::optimistic(60));
s.open(&bond("h", 1_000)).unwrap();
s.provision("h", BondOutcome::Honored).unwrap();
s.machine().clock().advance(61);
let (outcome, transfers) = s.finalize_and_settle("h").await.unwrap();
assert_eq!(outcome, BondOutcome::Honored);
assert!(transfers.is_empty(), "an honored bond disburses nothing");
}
#[test]
fn window_gates_finalization() {
let s = settler(SettlementConfig::optimistic(60));
s.open(&bond("d", 1_000)).unwrap();
s.provision("d", BondOutcome::Honored).unwrap();
assert!(s.machine().sweep().unwrap().is_empty(), "window still open");
}
#[test]
fn slash_distribution_uses_the_configured_routing() {
let routing = SlashRouting {
to_caller_bps: 4_000,
to_challengers_bps: 3_000,
to_insurance_bps: 2_000,
to_lineage_bps: 1_000,
};
let s = settler(SettlementConfig::optimistic(0).with_slash_routing(routing));
s.open(&bond("s", 1_003)).unwrap();
s.provision("s", BondOutcome::Slashed).unwrap();
let d = s.machine().slash_distribution("s").unwrap();
assert_eq!(d.caller.0 + d.challengers.0 + d.insurance.0 + d.lineage.0, 1_003);
assert_eq!(d.challengers, Usdc(300));
assert_eq!(d.insurance, Usdc(200));
assert_eq!(d.lineage, Usdc(100));
assert_eq!(d.caller, Usdc(403), "caller absorbs the dust");
}
}