1use std::collections::HashSet;
2
3use serde::{Deserialize, Serialize};
4
5use crate::error::Web3ContractError;
6use crate::identity::{validate_web3_identity_binding, SignedWeb3IdentityBinding};
7use crate::validation::{ensure_non_empty, ensure_unique_strings};
8
9pub const CHIO_WEB3_TRUST_PROFILE_SCHEMA: &str = "chio.web3-trust-profile.v1";
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub enum Web3SettlementPath {
14 DualSignature,
15 MerkleProof,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(rename_all = "snake_case")]
20pub enum Web3DisputePolicy {
21 OffChainArbitration,
22 TimeoutRefund,
23 BondSlash,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub enum Web3FinalityMode {
29 OptimisticL2,
30 L1Finalized,
31 SolanaConfirmed,
32}
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum Web3RegulatedRole {
37 Operator,
38 Custodian,
39 PaymentInstitution,
40 OracleOperator,
41 Arbitrator,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
45#[serde(deny_unknown_fields)]
46pub struct Web3DisputeWindow {
47 pub settlement_path: Web3SettlementPath,
48 pub challenge_window_secs: u64,
49 pub recovery_window_secs: u64,
50 pub dispute_policy: Web3DisputePolicy,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(deny_unknown_fields)]
55pub struct Web3ChainFinalityRule {
56 pub chain_id: String,
57 pub mode: Web3FinalityMode,
58 pub min_confirmations: u32,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize)]
62#[serde(deny_unknown_fields)]
63pub struct Web3RegulatedRoleAssumption {
64 pub role: Web3RegulatedRole,
65 pub actor_id: String,
66 pub responsibility: String,
67 pub custody_boundary_explicit: bool,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71#[serde(deny_unknown_fields)]
72pub struct Web3TrustProfile {
73 pub schema: String,
74 pub profile_id: String,
75 pub chio_contract_version: String,
76 pub primary_chain_id: String,
77 pub secondary_chain_ids: Vec<String>,
78 pub operator_binding: SignedWeb3IdentityBinding,
79 pub proof_bundle_required: bool,
80 pub dispute_windows: Vec<Web3DisputeWindow>,
81 pub finality_rules: Vec<Web3ChainFinalityRule>,
82 pub regulated_roles: Vec<Web3RegulatedRoleAssumption>,
83 pub custody_boundary_note: String,
84 pub local_policy_activation_required: bool,
85}
86
87pub fn validate_web3_trust_profile(profile: &Web3TrustProfile) -> Result<(), Web3ContractError> {
88 if profile.schema != CHIO_WEB3_TRUST_PROFILE_SCHEMA {
89 return Err(Web3ContractError::UnsupportedSchema(profile.schema.clone()));
90 }
91 ensure_non_empty(&profile.profile_id, "web3_trust_profile.profile_id")?;
92 ensure_non_empty(
93 &profile.chio_contract_version,
94 "web3_trust_profile.chio_contract_version",
95 )?;
96 ensure_non_empty(
97 &profile.primary_chain_id,
98 "web3_trust_profile.primary_chain_id",
99 )?;
100 ensure_non_empty(
101 &profile.custody_boundary_note,
102 "web3_trust_profile.custody_boundary_note",
103 )?;
104 validate_web3_identity_binding(&profile.operator_binding)?;
105 ensure_unique_strings(
106 &profile.secondary_chain_ids,
107 "web3_trust_profile.secondary_chain_ids",
108 )?;
109 if profile
110 .secondary_chain_ids
111 .iter()
112 .any(|chain_id| chain_id == &profile.primary_chain_id)
113 {
114 return Err(Web3ContractError::DuplicateValue(
115 profile.primary_chain_id.clone(),
116 ));
117 }
118
119 let mut known_chains = HashSet::new();
120 known_chains.insert(profile.primary_chain_id.as_str());
121 for chain_id in &profile.secondary_chain_ids {
122 known_chains.insert(chain_id.as_str());
123 }
124 for chain_id in &profile.operator_binding.certificate.chain_scope {
125 known_chains.insert(chain_id.as_str());
126 }
127 if !profile
128 .operator_binding
129 .certificate
130 .chain_scope
131 .iter()
132 .any(|chain_id| chain_id == &profile.primary_chain_id)
133 {
134 return Err(Web3ContractError::InvalidBinding(format!(
135 "binding does not cover primary chain {}",
136 profile.primary_chain_id
137 )));
138 }
139 for chain_id in &profile.secondary_chain_ids {
140 if !profile
141 .operator_binding
142 .certificate
143 .chain_scope
144 .iter()
145 .any(|candidate| candidate == chain_id)
146 {
147 return Err(Web3ContractError::InvalidBinding(format!(
148 "binding does not cover secondary chain {}",
149 chain_id
150 )));
151 }
152 }
153
154 if !profile.local_policy_activation_required {
155 return Err(Web3ContractError::InvalidBinding(
156 "web3 trust profile must require explicit local policy activation".to_string(),
157 ));
158 }
159
160 if profile.dispute_windows.is_empty() {
161 return Err(Web3ContractError::MissingField(
162 "web3_trust_profile.dispute_windows",
163 ));
164 }
165 let mut seen_paths = HashSet::new();
166 for window in &profile.dispute_windows {
167 if !seen_paths.insert(window.settlement_path) {
168 return Err(Web3ContractError::DuplicateValue(format!(
169 "web3_trust_profile.dispute_windows:{:?}",
170 window.settlement_path
171 )));
172 }
173 if window.challenge_window_secs == 0 || window.recovery_window_secs == 0 {
174 return Err(Web3ContractError::InvalidBinding(format!(
175 "dispute window {:?} must have non-zero durations",
176 window.settlement_path
177 )));
178 }
179 }
180 if profile.finality_rules.is_empty() {
181 return Err(Web3ContractError::MissingField(
182 "web3_trust_profile.finality_rules",
183 ));
184 }
185 let mut seen_finality = HashSet::new();
186 for rule in &profile.finality_rules {
187 ensure_non_empty(&rule.chain_id, "web3_trust_profile.finality_rules.chain_id")?;
188 if rule.min_confirmations == 0 {
189 return Err(Web3ContractError::InvalidBinding(format!(
190 "finality rule {} must require at least one confirmation",
191 rule.chain_id
192 )));
193 }
194 if !seen_finality.insert(rule.chain_id.as_str()) {
195 return Err(Web3ContractError::DuplicateValue(rule.chain_id.clone()));
196 }
197 }
198 for chain_id in [&profile.primary_chain_id]
199 .into_iter()
200 .chain(profile.secondary_chain_ids.iter())
201 {
202 if !seen_finality.contains(chain_id.as_str()) {
203 return Err(Web3ContractError::UnknownReference(chain_id.clone()));
204 }
205 }
206
207 if profile.regulated_roles.is_empty() {
208 return Err(Web3ContractError::MissingField(
209 "web3_trust_profile.regulated_roles",
210 ));
211 }
212 let mut saw_custodian = false;
213 for role in &profile.regulated_roles {
214 ensure_non_empty(
215 &role.actor_id,
216 "web3_trust_profile.regulated_roles.actor_id",
217 )?;
218 ensure_non_empty(
219 &role.responsibility,
220 "web3_trust_profile.regulated_roles.responsibility",
221 )?;
222 if !role.custody_boundary_explicit {
223 return Err(Web3ContractError::InvalidBinding(format!(
224 "regulated role {:?} must keep custody boundary explicit",
225 role.role
226 )));
227 }
228 if role.role == Web3RegulatedRole::Custodian {
229 saw_custodian = true;
230 }
231 }
232 if !saw_custodian {
233 return Err(Web3ContractError::InvalidBinding(
234 "web3 trust profile must record at least one custodian role".to_string(),
235 ));
236 }
237
238 Ok(())
239}