1use chio_core_types::canonical_json_bytes;
2use chio_core_types::capability::scope::MonetaryAmount;
3use chio_core_types::crypto::sha256_hex;
4use chio_core_types::receipt::lineage::SignedExportEnvelope;
5use chio_listing::{normalize_namespace, GenericListingActorKind, GenericTrustAdmissionClass};
6use serde::{Deserialize, Serialize};
7
8pub const OPEN_MARKET_FEE_SCHEDULE_ARTIFACT_SCHEMA: &str = "chio.registry.market-fee-schedule.v1";
9
10#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
11#[serde(rename_all = "snake_case")]
12pub enum OpenMarketBondClass {
13 Publication,
14 Listing,
15 Dispute,
16}
17
18#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
19#[serde(rename_all = "snake_case")]
20pub enum OpenMarketCollateralReferenceKind {
21 CreditBond,
22 ExternalReference,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct OpenMarketEconomicsScope {
28 pub namespace: String,
29 #[serde(default, skip_serializing_if = "Vec::is_empty")]
30 pub allowed_listing_operator_ids: Vec<String>,
31 #[serde(default, skip_serializing_if = "Vec::is_empty")]
32 pub allowed_actor_kinds: Vec<GenericListingActorKind>,
33 #[serde(default, skip_serializing_if = "Vec::is_empty")]
34 pub allowed_admission_classes: Vec<GenericTrustAdmissionClass>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub policy_reference: Option<String>,
37}
38
39impl OpenMarketEconomicsScope {
40 pub fn validate(&self) -> Result<(), String> {
41 validate_non_empty(&self.namespace, "scope.namespace")?;
42 for (index, operator_id) in self.allowed_listing_operator_ids.iter().enumerate() {
43 validate_non_empty(
44 operator_id,
45 &format!("scope.allowed_listing_operator_ids[{index}]"),
46 )?;
47 }
48 Ok(())
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
53#[serde(rename_all = "camelCase", deny_unknown_fields)]
54pub struct OpenMarketBondRequirement {
55 pub bond_class: OpenMarketBondClass,
56 pub required_amount: MonetaryAmount,
57 pub collateral_reference_kind: OpenMarketCollateralReferenceKind,
58 pub slashable: bool,
59}
60
61impl OpenMarketBondRequirement {
62 pub fn validate(&self, field: &str) -> Result<(), String> {
63 validate_monetary_amount(&self.required_amount, &format!("{field}.required_amount"))
64 }
65}
66
67#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
68#[serde(rename_all = "camelCase", deny_unknown_fields)]
69pub struct OpenMarketFeeScheduleArtifact {
70 pub schema: String,
71 pub fee_schedule_id: String,
72 pub namespace: String,
73 pub governing_operator_id: String,
74 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub governing_operator_name: Option<String>,
76 pub scope: OpenMarketEconomicsScope,
77 pub publication_fee: MonetaryAmount,
78 pub dispute_fee: MonetaryAmount,
79 pub market_participation_fee: MonetaryAmount,
80 pub bond_requirements: Vec<OpenMarketBondRequirement>,
81 pub issued_at: u64,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
83 pub expires_at: Option<u64>,
84 pub issued_by: String,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub note: Option<String>,
87}
88
89impl OpenMarketFeeScheduleArtifact {
90 pub fn validate(&self) -> Result<(), String> {
91 if self.schema != OPEN_MARKET_FEE_SCHEDULE_ARTIFACT_SCHEMA {
92 return Err(format!(
93 "unsupported open-market fee schedule schema: {}",
94 self.schema
95 ));
96 }
97 validate_non_empty(&self.fee_schedule_id, "fee_schedule_id")?;
98 validate_non_empty(&self.namespace, "namespace")?;
99 validate_non_empty(&self.governing_operator_id, "governing_operator_id")?;
100 validate_non_empty(&self.issued_by, "issued_by")?;
101 self.scope.validate()?;
102 if normalize_namespace(&self.namespace) != normalize_namespace(&self.scope.namespace) {
103 return Err("fee schedule namespace must match scope namespace".to_string());
104 }
105 validate_monetary_amount(&self.publication_fee, "publication_fee")?;
106 validate_monetary_amount(&self.dispute_fee, "dispute_fee")?;
107 validate_monetary_amount(&self.market_participation_fee, "market_participation_fee")?;
108 if self.bond_requirements.is_empty() {
109 return Err("bond_requirements must not be empty".to_string());
110 }
111 for (index, requirement) in self.bond_requirements.iter().enumerate() {
112 requirement.validate(&format!("bond_requirements[{index}]"))?;
113 }
114 if let Some(expires_at) = self.expires_at {
115 if expires_at <= self.issued_at {
116 return Err("expires_at must be greater than issued_at".to_string());
117 }
118 }
119 Ok(())
120 }
121}
122
123pub type SignedOpenMarketFeeSchedule = SignedExportEnvelope<OpenMarketFeeScheduleArtifact>;
124
125#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
126#[serde(rename_all = "camelCase", deny_unknown_fields)]
127pub struct OpenMarketFeeScheduleIssueRequest {
128 pub scope: OpenMarketEconomicsScope,
129 pub publication_fee: MonetaryAmount,
130 pub dispute_fee: MonetaryAmount,
131 pub market_participation_fee: MonetaryAmount,
132 pub bond_requirements: Vec<OpenMarketBondRequirement>,
133 pub issued_by: String,
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 pub issued_at: Option<u64>,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
137 pub expires_at: Option<u64>,
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 pub note: Option<String>,
140}
141
142impl OpenMarketFeeScheduleIssueRequest {
143 pub fn validate(&self) -> Result<(), String> {
144 self.scope.validate()?;
145 validate_non_empty(&self.issued_by, "issued_by")?;
146 validate_monetary_amount(&self.publication_fee, "publication_fee")?;
147 validate_monetary_amount(&self.dispute_fee, "dispute_fee")?;
148 validate_monetary_amount(&self.market_participation_fee, "market_participation_fee")?;
149 if self.bond_requirements.is_empty() {
150 return Err("bond_requirements must not be empty".to_string());
151 }
152 for (index, requirement) in self.bond_requirements.iter().enumerate() {
153 requirement.validate(&format!("bond_requirements[{index}]"))?;
154 }
155 Ok(())
156 }
157}
158
159pub fn build_open_market_fee_schedule_artifact(
160 local_operator_id: &str,
161 local_operator_name: Option<String>,
162 request: &OpenMarketFeeScheduleIssueRequest,
163 issued_at: u64,
164) -> Result<OpenMarketFeeScheduleArtifact, String> {
165 request.validate()?;
166 validate_non_empty(local_operator_id, "local_operator_id")?;
167 let issued_at = request.issued_at.unwrap_or(issued_at);
168 let fee_schedule_id = format!(
169 "market-fee-schedule-{}",
170 sha256_hex(
171 &canonical_json_bytes(&(
172 local_operator_id,
173 normalize_namespace(&request.scope.namespace),
174 &request.publication_fee,
175 &request.dispute_fee,
176 &request.market_participation_fee,
177 &request.bond_requirements,
178 issued_at,
179 ))
180 .map_err(|error| error.to_string())?
181 )
182 );
183 let artifact = OpenMarketFeeScheduleArtifact {
184 schema: OPEN_MARKET_FEE_SCHEDULE_ARTIFACT_SCHEMA.to_string(),
185 fee_schedule_id,
186 namespace: request.scope.namespace.clone(),
187 governing_operator_id: local_operator_id.to_string(),
188 governing_operator_name: local_operator_name,
189 scope: request.scope.clone(),
190 publication_fee: request.publication_fee.clone(),
191 dispute_fee: request.dispute_fee.clone(),
192 market_participation_fee: request.market_participation_fee.clone(),
193 bond_requirements: request.bond_requirements.clone(),
194 issued_at,
195 expires_at: request.expires_at,
196 issued_by: request.issued_by.clone(),
197 note: request.note.clone(),
198 };
199 artifact.validate()?;
200 Ok(artifact)
201}
202
203fn validate_monetary_amount(value: &MonetaryAmount, field: &str) -> Result<(), String> {
204 if value.units == 0 {
205 return Err(format!("{field}.units must be greater than zero"));
206 }
207 validate_non_empty(&value.currency, &format!("{field}.currency"))
208}
209
210fn validate_non_empty(value: &str, field: &str) -> Result<(), String> {
211 if value.trim().is_empty() {
212 Err(format!("{field} must not be empty"))
213 } else {
214 Ok(())
215 }
216}