use serde::{Deserialize, Serialize};
use crate::identity::KycTier;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RegRegime {
RegS,
RegD,
MiFidII,
Unrestricted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Side {
Buy,
Sell,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AssetWeight {
pub asset_id: String,
pub weight_bps: u16,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum Objective {
Acquire {
asset_id: String,
target_notional: u128,
max_unit_price: u128,
},
Exit {
asset_id: String,
quantity: u128,
min_unit_price: u128,
},
Rebalance { targets: Vec<AssetWeight> },
Hedge {
asset_id: String,
notional: u128,
instrument: String,
},
Yield {
asset_id: String,
amount: u128,
min_apy_bps: u32,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Constraints {
pub max_slippage_bps: u16,
pub deadline_unix: i64,
#[serde(default)]
pub allowed_venues: Vec<String>,
#[serde(default)]
pub allowed_chains: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ComplianceReq {
pub reg_regime: RegRegime,
pub min_kyc_tier: KycTier,
#[serde(default)]
pub accredited_only: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Authorization {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ap2_mandate_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub delegation_scope_ref: Option<String>,
pub max_total_notional: u128,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SettlementReq {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proof: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preferred_route: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapitalIntent {
pub intent_id: String,
pub principal_did: String,
pub objective: Objective,
pub constraints: Constraints,
pub compliance: ComplianceReq,
pub authorization: Authorization,
pub settlement: SettlementReq,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub signature: Vec<u8>,
}
impl CapitalIntent {
pub fn signing_payload(&self) -> Vec<u8> {
fn push_str(buf: &mut Vec<u8>, s: &str) {
buf.extend_from_slice(&(s.len() as u32).to_be_bytes());
buf.extend_from_slice(s.as_bytes());
}
let mut buf = Vec::new();
buf.extend_from_slice(b"tenzro-capital-intent:v1");
push_str(&mut buf, &self.intent_id);
push_str(&mut buf, &self.principal_did);
push_str(&mut buf, &serde_objective_tag(&self.objective));
buf.extend_from_slice(&self.authorization.max_total_notional.to_be_bytes());
buf.extend_from_slice(&self.constraints.deadline_unix.to_be_bytes());
buf.extend_from_slice(&(self.constraints.max_slippage_bps).to_be_bytes());
push_str(&mut buf, self.compliance.reg_regime.as_str());
push_str(&mut buf, self.compliance.min_kyc_tier.as_str());
buf.push(self.compliance.accredited_only as u8);
buf
}
}
impl RegRegime {
pub fn as_str(&self) -> &'static str {
match self {
RegRegime::RegS => "reg_s",
RegRegime::RegD => "reg_d",
RegRegime::MiFidII => "mifid_ii",
RegRegime::Unrestricted => "unrestricted",
}
}
}
fn serde_objective_tag(o: &Objective) -> String {
match o {
Objective::Acquire { asset_id, target_notional, max_unit_price } => {
format!("acquire:{asset_id}:{target_notional}:{max_unit_price}")
}
Objective::Exit { asset_id, quantity, min_unit_price } => {
format!("exit:{asset_id}:{quantity}:{min_unit_price}")
}
Objective::Rebalance { targets } => {
let mut s = String::from("rebalance");
for t in targets {
s.push_str(&format!(":{}={}", t.asset_id, t.weight_bps));
}
s
}
Objective::Hedge { asset_id, notional, instrument } => {
format!("hedge:{asset_id}:{notional}:{instrument}")
}
Objective::Yield { asset_id, amount, min_apy_bps } => {
format!("yield:{asset_id}:{amount}:{min_apy_bps}")
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapitalQuote {
pub intent_id: String,
pub solver_did: String,
pub plan: String,
pub price: u128,
pub eta_secs: u64,
pub quoted_at: i64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CapitalIntentStatus {
Open,
Quoting,
Assigned,
Executing,
Verifying,
Settled,
Compensating,
Failed,
Expired,
}
impl CapitalIntentStatus {
pub fn is_terminal(&self) -> bool {
matches!(
self,
CapitalIntentStatus::Settled
| CapitalIntentStatus::Failed
| CapitalIntentStatus::Expired
)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum LegStatus {
Planned,
Settled,
Compensated,
Failed,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct VenueQuote {
pub venue: String,
pub unit_price: u128,
}
pub fn best_execution_ok(side: Side, chosen: u128, quotes: &[VenueQuote]) -> bool {
if quotes.is_empty() {
return false;
}
match side {
Side::Buy => chosen <= quotes.iter().map(|q| q.unit_price).min().unwrap_or(chosen),
Side::Sell => chosen >= quotes.iter().map(|q| q.unit_price).max().unwrap_or(chosen),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapitalLeg {
pub venue: String,
pub asset_id: String,
pub side: Side,
pub quantity: u128,
pub unit_price: u128,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub settlement_ref: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub proof: Option<String>,
#[serde(default)]
pub venue_quotes: Vec<VenueQuote>,
#[serde(default)]
pub best_execution_verified: bool,
pub status: LegStatus,
pub updated_at: i64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CapitalIntentRecord {
pub intent: CapitalIntent,
pub status: CapitalIntentStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub solver_did: Option<String>,
#[serde(default)]
pub quotes: Vec<CapitalQuote>,
#[serde(default)]
pub legs: Vec<CapitalLeg>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub escrow_id: Option<String>,
#[serde(default)]
pub filled_notional: u128,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub receipt: Option<String>,
pub created_at: i64,
pub updated_at: i64,
}
impl CapitalIntentRecord {
pub fn all_legs_settled(&self) -> bool {
!self.legs.is_empty()
&& self.legs.iter().all(|l| l.status == LegStatus::Settled)
}
pub fn is_terminal(&self) -> bool {
self.status.is_terminal()
}
}