Skip to main content

chio_web3/
lib.rs

1//! Chio web3 settlement, anchoring, and official-chain contract types.
2//!
3//! These types freeze the first official web3 execution surface on top of the
4//! Chio extension substrate. They define the trust profile, contract package,
5//! chain configuration, anchoring proof bundle, oracle evidence envelope, and
6//! web3 settlement lifecycle artifacts that later live-money work must honor.
7
8pub use chio_core_types::{canonical, capability, crypto, hashing, merkle, receipt};
9pub use chio_credit as credit;
10
11use std::collections::HashSet;
12
13pub use chio_core_types::oracle::OracleConversionEvidence;
14use serde::{Deserialize, Serialize};
15
16use crate::canonical::canonical_json_bytes;
17use crate::capability::MonetaryAmount;
18use crate::credit::{
19    CapitalExecutionInstructionAction, CapitalExecutionRailKind, CapitalExecutionReconciledState,
20    CreditBondLifecycleState, SignedCapitalExecutionInstruction, SignedCreditBond,
21    CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA,
22};
23use crate::crypto::{PublicKey, Signature};
24use crate::hashing::Hash;
25use crate::merkle::{leaf_hash, MerkleProof};
26use crate::receipt::{ChioReceipt, SignedExportEnvelope};
27
28pub const CHIO_KEY_BINDING_CERTIFICATE_SCHEMA: &str = "chio.key-binding-certificate.v1";
29pub const CHIO_WEB3_TRUST_PROFILE_SCHEMA: &str = "chio.web3-trust-profile.v1";
30pub const CHIO_WEB3_CONTRACT_PACKAGE_SCHEMA: &str = "chio.web3-contract-package.v1";
31pub const CHIO_WEB3_CHAIN_CONFIGURATION_SCHEMA: &str = "chio.web3-chain-configuration.v1";
32pub const CHIO_CHECKPOINT_STATEMENT_SCHEMA: &str = "chio.checkpoint_statement.v1";
33pub const CHIO_ANCHOR_INCLUSION_PROOF_SCHEMA: &str = "chio.anchor-inclusion-proof.v1";
34pub const CHIO_ORACLE_CONVERSION_EVIDENCE_SCHEMA: &str = "chio.oracle-conversion-evidence.v1";
35pub const CHIO_LINK_ORACLE_AUTHORITY: &str = "chio_link_runtime_v1";
36pub const CHIO_WEB3_SETTLEMENT_DISPATCH_SCHEMA: &str = "chio.web3-settlement-dispatch.v1";
37pub const CHIO_WEB3_SETTLEMENT_RECEIPT_SCHEMA: &str = "chio.web3-settlement-execution-receipt.v1";
38pub const CHIO_WEB3_QUALIFICATION_MATRIX_SCHEMA: &str = "chio.web3-qualification-matrix.v1";
39pub const CHIO_LINK_CONTROL_STATE_SCHEMA: &str = "chio.link.control-state.v1";
40pub const CHIO_LINK_CONTROL_TRACE_SCHEMA: &str = "chio.link.control-trace.v1";
41pub const CHIO_ANCHOR_CONTROL_STATE_SCHEMA: &str = "chio.anchor.control-state.v1";
42pub const CHIO_ANCHOR_CONTROL_TRACE_SCHEMA: &str = "chio.anchor.control-trace.v1";
43pub const CHIO_SETTLE_CONTROL_STATE_SCHEMA: &str = "chio.settle.control-state.v1";
44pub const CHIO_SETTLE_CONTROL_TRACE_SCHEMA: &str = "chio.settle.control-trace.v1";
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub enum Web3KeyBindingPurpose {
49    Anchor,
50    Settle,
51}
52
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
54#[serde(rename_all = "snake_case")]
55pub enum Web3SettlementPath {
56    DualSignature,
57    MerkleProof,
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum Web3DisputePolicy {
63    OffChainArbitration,
64    TimeoutRefund,
65    BondSlash,
66}
67
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69#[serde(rename_all = "snake_case")]
70pub enum Web3FinalityMode {
71    OptimisticL2,
72    L1Finalized,
73    SolanaConfirmed,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
77#[serde(rename_all = "snake_case")]
78pub enum Web3RegulatedRole {
79    Operator,
80    Custodian,
81    PaymentInstitution,
82    OracleOperator,
83    Arbitrator,
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
87#[serde(rename_all = "snake_case")]
88pub enum Web3ContractKind {
89    RootRegistry,
90    Escrow,
91    BondVault,
92    IdentityRegistry,
93    PriceResolver,
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
97#[serde(rename_all = "snake_case")]
98pub enum Web3BindingLanguage {
99    Rust,
100}
101
102#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
103#[serde(rename_all = "snake_case")]
104pub enum Web3ChainRole {
105    Primary,
106    Secondary,
107}
108
109#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
110#[serde(rename_all = "snake_case")]
111pub enum Web3SettlementLifecycleState {
112    PendingDispatch,
113    EscrowLocked,
114    PartiallySettled,
115    Settled,
116    Reversed,
117    ChargedBack,
118    TimedOut,
119    Failed,
120    Reorged,
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum Web3QualificationOutcome {
126    Pass,
127    FailClosed,
128}
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(deny_unknown_fields)]
132pub struct Web3IdentityBindingCertificate {
133    pub schema: String,
134    pub chio_identity: String,
135    pub chio_public_key: PublicKey,
136    pub chain_scope: Vec<String>,
137    pub purpose: Vec<Web3KeyBindingPurpose>,
138    pub settlement_address: String,
139    pub issued_at: u64,
140    pub expires_at: u64,
141    pub nonce: String,
142}
143
144#[derive(Debug, Clone, Serialize, Deserialize)]
145#[serde(deny_unknown_fields)]
146pub struct SignedWeb3IdentityBinding {
147    pub certificate: Web3IdentityBindingCertificate,
148    pub signature: Signature,
149}
150
151#[derive(Debug, Clone, Serialize, Deserialize)]
152#[serde(deny_unknown_fields)]
153pub struct Web3DisputeWindow {
154    pub settlement_path: Web3SettlementPath,
155    pub challenge_window_secs: u64,
156    pub recovery_window_secs: u64,
157    pub dispute_policy: Web3DisputePolicy,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
161#[serde(deny_unknown_fields)]
162pub struct Web3ChainFinalityRule {
163    pub chain_id: String,
164    pub mode: Web3FinalityMode,
165    pub min_confirmations: u32,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize)]
169#[serde(deny_unknown_fields)]
170pub struct Web3RegulatedRoleAssumption {
171    pub role: Web3RegulatedRole,
172    pub actor_id: String,
173    pub responsibility: String,
174    pub custody_boundary_explicit: bool,
175}
176
177#[derive(Debug, Clone, Serialize, Deserialize)]
178#[serde(deny_unknown_fields)]
179pub struct Web3TrustProfile {
180    pub schema: String,
181    pub profile_id: String,
182    pub chio_contract_version: String,
183    pub primary_chain_id: String,
184    pub secondary_chain_ids: Vec<String>,
185    pub operator_binding: SignedWeb3IdentityBinding,
186    pub proof_bundle_required: bool,
187    pub dispute_windows: Vec<Web3DisputeWindow>,
188    pub finality_rules: Vec<Web3ChainFinalityRule>,
189    pub regulated_roles: Vec<Web3RegulatedRoleAssumption>,
190    pub custody_boundary_note: String,
191    pub local_policy_activation_required: bool,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
195#[serde(deny_unknown_fields)]
196pub struct Web3ContractInterface {
197    pub contract_id: String,
198    pub kind: Web3ContractKind,
199    pub interface_name: String,
200    pub abi_reference: String,
201    pub implementation_reference: String,
202    pub immutable: bool,
203}
204
205#[derive(Debug, Clone, Serialize, Deserialize)]
206#[serde(deny_unknown_fields)]
207pub struct Web3BindingTarget {
208    pub language: Web3BindingLanguage,
209    pub crate_path: String,
210    pub module_name: String,
211    pub contract_ids: Vec<String>,
212}
213
214#[derive(Debug, Clone, Serialize, Deserialize)]
215#[serde(deny_unknown_fields)]
216pub struct Web3ContractPackage {
217    pub schema: String,
218    pub package_id: String,
219    pub version: String,
220    pub chio_contract_version: String,
221    pub contracts: Vec<Web3ContractInterface>,
222    pub bindings: Vec<Web3BindingTarget>,
223    pub deferred_capabilities: Vec<String>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227#[serde(deny_unknown_fields)]
228pub struct Web3ChainDeployment {
229    pub chain_id: String,
230    pub network_name: String,
231    pub role: Web3ChainRole,
232    pub settlement_token_symbol: String,
233    pub settlement_token_address: String,
234    pub root_registry_address: String,
235    pub escrow_address: String,
236    pub bond_vault_address: String,
237    pub identity_registry_address: String,
238    pub price_resolver_address: String,
239    pub operator_address: String,
240}
241
242#[derive(Debug, Clone, Serialize, Deserialize)]
243#[serde(deny_unknown_fields)]
244pub struct Web3ChainGasProfile {
245    pub chain_id: String,
246    pub publish_root_gas: u64,
247    pub dual_sign_settlement_gas: u64,
248    pub merkle_settlement_gas: u64,
249    pub bond_release_gas: u64,
250    pub price_read_gas: u64,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize)]
254#[serde(deny_unknown_fields)]
255pub struct Web3ChainConfiguration {
256    pub schema: String,
257    pub package_id: String,
258    pub primary_chain_id: String,
259    pub deployments: Vec<Web3ChainDeployment>,
260    pub gas_profiles: Vec<Web3ChainGasProfile>,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
264#[serde(deny_unknown_fields)]
265pub struct Web3ReceiptInclusion {
266    pub checkpoint_seq: u64,
267    pub merkle_root: Hash,
268    pub proof: MerkleProof,
269}
270
271#[derive(Debug, Clone, Serialize, Deserialize)]
272#[serde(deny_unknown_fields)]
273pub struct Web3CheckpointStatement {
274    pub schema: String,
275    pub checkpoint_seq: u64,
276    pub batch_start_seq: u64,
277    pub batch_end_seq: u64,
278    pub tree_size: u64,
279    pub merkle_root: Hash,
280    pub issued_at: u64,
281    #[serde(default, skip_serializing_if = "Option::is_none")]
282    pub previous_checkpoint_sha256: Option<String>,
283    pub kernel_key: PublicKey,
284    pub signature: Signature,
285}
286
287#[derive(Debug, Clone, Serialize, Deserialize)]
288#[serde(deny_unknown_fields)]
289pub struct Web3ChainAnchorRecord {
290    pub chain_id: String,
291    pub contract_address: String,
292    pub operator_address: String,
293    pub tx_hash: String,
294    pub block_number: u64,
295    pub block_hash: String,
296    pub anchored_merkle_root: Hash,
297    pub anchored_checkpoint_seq: u64,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
301#[serde(deny_unknown_fields)]
302pub struct Web3BitcoinAnchor {
303    pub method: String,
304    pub ots_proof_b64: String,
305    pub bitcoin_block_height: u64,
306    pub bitcoin_block_hash: String,
307}
308
309#[derive(Debug, Clone, Serialize, Deserialize)]
310#[serde(deny_unknown_fields)]
311pub struct Web3SuperRootInclusion {
312    pub super_root: Hash,
313    pub proof: MerkleProof,
314    pub aggregated_checkpoint_start: u64,
315    pub aggregated_checkpoint_end: u64,
316}
317
318#[derive(Debug, Clone, Serialize, Deserialize)]
319#[serde(deny_unknown_fields)]
320pub struct AnchorInclusionProof {
321    pub schema: String,
322    pub receipt: ChioReceipt,
323    pub receipt_inclusion: Web3ReceiptInclusion,
324    pub checkpoint_statement: Web3CheckpointStatement,
325    #[serde(default, skip_serializing_if = "Option::is_none")]
326    pub chain_anchor: Option<Web3ChainAnchorRecord>,
327    #[serde(default, skip_serializing_if = "Option::is_none")]
328    pub bitcoin_anchor: Option<Web3BitcoinAnchor>,
329    #[serde(default, skip_serializing_if = "Option::is_none")]
330    pub super_root_inclusion: Option<Web3SuperRootInclusion>,
331    pub key_binding_certificate: SignedWeb3IdentityBinding,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
335#[serde(deny_unknown_fields)]
336pub struct Web3SettlementSupportBoundary {
337    pub real_dispatch_supported: bool,
338    pub anchor_proof_required: bool,
339    pub oracle_evidence_required_for_fx: bool,
340    pub custody_boundary_explicit: bool,
341    pub reversal_supported: bool,
342}
343
344#[derive(Debug, Clone, Serialize, Deserialize)]
345#[serde(deny_unknown_fields)]
346pub struct Web3SettlementDispatchArtifact {
347    pub schema: String,
348    pub dispatch_id: String,
349    pub issued_at: u64,
350    pub trust_profile_id: String,
351    pub contract_package_id: String,
352    pub chain_id: String,
353    pub capital_instruction: SignedCapitalExecutionInstruction,
354    #[serde(default, skip_serializing_if = "Option::is_none")]
355    pub bond: Option<SignedCreditBond>,
356    pub settlement_path: Web3SettlementPath,
357    pub settlement_amount: MonetaryAmount,
358    pub escrow_id: String,
359    pub escrow_contract: String,
360    pub bond_vault_contract: String,
361    pub beneficiary_address: String,
362    pub support_boundary: Web3SettlementSupportBoundary,
363    #[serde(default, skip_serializing_if = "Option::is_none")]
364    pub note: Option<String>,
365}
366
367pub type SignedWeb3SettlementDispatch = SignedExportEnvelope<Web3SettlementDispatchArtifact>;
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
370#[serde(deny_unknown_fields)]
371pub struct Web3SettlementExecutionReceiptArtifact {
372    pub schema: String,
373    pub execution_receipt_id: String,
374    pub issued_at: u64,
375    pub dispatch: Web3SettlementDispatchArtifact,
376    pub observed_execution: crate::credit::CapitalExecutionObservation,
377    pub lifecycle_state: Web3SettlementLifecycleState,
378    pub settlement_reference: String,
379    #[serde(default, skip_serializing_if = "Option::is_none")]
380    pub reconciled_anchor_proof: Option<AnchorInclusionProof>,
381    #[serde(default, skip_serializing_if = "Option::is_none")]
382    pub oracle_evidence: Option<OracleConversionEvidence>,
383    pub settled_amount: MonetaryAmount,
384    #[serde(default, skip_serializing_if = "Option::is_none")]
385    pub reversal_of: Option<String>,
386    #[serde(default, skip_serializing_if = "Option::is_none")]
387    pub failure_reason: Option<String>,
388    #[serde(default, skip_serializing_if = "Option::is_none")]
389    pub note: Option<String>,
390}
391
392pub type SignedWeb3SettlementExecutionReceipt =
393    SignedExportEnvelope<Web3SettlementExecutionReceiptArtifact>;
394
395#[derive(Debug, Clone, Serialize, Deserialize)]
396#[serde(deny_unknown_fields)]
397pub struct Web3QualificationCase {
398    pub id: String,
399    pub name: String,
400    pub requirement_ids: Vec<String>,
401    pub lifecycle_state: Web3SettlementLifecycleState,
402    pub expected_outcome: Web3QualificationOutcome,
403    pub observed_outcome: Web3QualificationOutcome,
404    pub notes: String,
405}
406
407#[derive(Debug, Clone, Serialize, Deserialize)]
408#[serde(deny_unknown_fields)]
409pub struct Web3QualificationMatrix {
410    pub schema: String,
411    pub trust_profile_id: String,
412    pub contract_package_id: String,
413    pub cases: Vec<Web3QualificationCase>,
414}
415
416#[derive(Debug, thiserror::Error, PartialEq, Eq)]
417pub enum Web3ContractError {
418    #[error("unsupported schema: {0}")]
419    UnsupportedSchema(String),
420
421    #[error("missing field: {0}")]
422    MissingField(&'static str),
423
424    #[error("duplicate id or value: {0}")]
425    DuplicateValue(String),
426
427    #[error("unknown reference: {0}")]
428    UnknownReference(String),
429
430    #[error("invalid binding: {0}")]
431    InvalidBinding(String),
432
433    #[error("invalid proof: {0}")]
434    InvalidProof(String),
435
436    #[error("invalid settlement: {0}")]
437    InvalidSettlement(String),
438
439    #[error("invalid qualification case: {0}")]
440    InvalidQualificationCase(String),
441}
442
443pub fn validate_web3_identity_binding(
444    binding: &SignedWeb3IdentityBinding,
445) -> Result<(), Web3ContractError> {
446    if binding.certificate.schema != CHIO_KEY_BINDING_CERTIFICATE_SCHEMA {
447        return Err(Web3ContractError::UnsupportedSchema(
448            binding.certificate.schema.clone(),
449        ));
450    }
451    ensure_non_empty(&binding.certificate.chio_identity, "binding.chio_identity")?;
452    ensure_non_empty(
453        &binding.certificate.settlement_address,
454        "binding.settlement_address",
455    )?;
456    ensure_non_empty(&binding.certificate.nonce, "binding.nonce")?;
457    if binding.certificate.chain_scope.is_empty() {
458        return Err(Web3ContractError::MissingField("binding.chain_scope"));
459    }
460    if binding.certificate.purpose.is_empty() {
461        return Err(Web3ContractError::MissingField("binding.purpose"));
462    }
463    ensure_unique_strings(&binding.certificate.chain_scope, "binding.chain_scope")?;
464    ensure_unique_copy_values(&binding.certificate.purpose, "binding.purpose")?;
465    if binding.certificate.issued_at >= binding.certificate.expires_at {
466        return Err(Web3ContractError::InvalidBinding(
467            "identity binding issued_at must be earlier than expires_at".to_string(),
468        ));
469    }
470    Ok(())
471}
472
473pub fn verify_web3_identity_binding(
474    binding: &SignedWeb3IdentityBinding,
475) -> Result<(), Web3ContractError> {
476    validate_web3_identity_binding(binding)?;
477    let verified = binding
478        .certificate
479        .chio_public_key
480        .verify_canonical(&binding.certificate, &binding.signature)
481        .map_err(|error| Web3ContractError::InvalidBinding(error.to_string()))?;
482    if !verified {
483        return Err(Web3ContractError::InvalidBinding(
484            "identity binding signature verification failed".to_string(),
485        ));
486    }
487    Ok(())
488}
489
490pub fn validate_web3_trust_profile(profile: &Web3TrustProfile) -> Result<(), Web3ContractError> {
491    if profile.schema != CHIO_WEB3_TRUST_PROFILE_SCHEMA {
492        return Err(Web3ContractError::UnsupportedSchema(profile.schema.clone()));
493    }
494    ensure_non_empty(&profile.profile_id, "web3_trust_profile.profile_id")?;
495    ensure_non_empty(
496        &profile.chio_contract_version,
497        "web3_trust_profile.chio_contract_version",
498    )?;
499    ensure_non_empty(
500        &profile.primary_chain_id,
501        "web3_trust_profile.primary_chain_id",
502    )?;
503    ensure_non_empty(
504        &profile.custody_boundary_note,
505        "web3_trust_profile.custody_boundary_note",
506    )?;
507    validate_web3_identity_binding(&profile.operator_binding)?;
508    ensure_unique_strings(
509        &profile.secondary_chain_ids,
510        "web3_trust_profile.secondary_chain_ids",
511    )?;
512    if profile
513        .secondary_chain_ids
514        .iter()
515        .any(|chain_id| chain_id == &profile.primary_chain_id)
516    {
517        return Err(Web3ContractError::DuplicateValue(
518            profile.primary_chain_id.clone(),
519        ));
520    }
521
522    let mut known_chains = HashSet::new();
523    known_chains.insert(profile.primary_chain_id.as_str());
524    for chain_id in &profile.secondary_chain_ids {
525        known_chains.insert(chain_id.as_str());
526    }
527    for chain_id in &profile.operator_binding.certificate.chain_scope {
528        known_chains.insert(chain_id.as_str());
529    }
530    if !profile
531        .operator_binding
532        .certificate
533        .chain_scope
534        .iter()
535        .any(|chain_id| chain_id == &profile.primary_chain_id)
536    {
537        return Err(Web3ContractError::InvalidBinding(format!(
538            "binding does not cover primary chain {}",
539            profile.primary_chain_id
540        )));
541    }
542    for chain_id in &profile.secondary_chain_ids {
543        if !profile
544            .operator_binding
545            .certificate
546            .chain_scope
547            .iter()
548            .any(|candidate| candidate == chain_id)
549        {
550            return Err(Web3ContractError::InvalidBinding(format!(
551                "binding does not cover secondary chain {}",
552                chain_id
553            )));
554        }
555    }
556
557    if !profile.local_policy_activation_required {
558        return Err(Web3ContractError::InvalidBinding(
559            "web3 trust profile must require explicit local policy activation".to_string(),
560        ));
561    }
562
563    if profile.dispute_windows.is_empty() {
564        return Err(Web3ContractError::MissingField(
565            "web3_trust_profile.dispute_windows",
566        ));
567    }
568    let mut seen_paths = HashSet::new();
569    for window in &profile.dispute_windows {
570        if !seen_paths.insert(window.settlement_path) {
571            return Err(Web3ContractError::DuplicateValue(format!(
572                "web3_trust_profile.dispute_windows:{:?}",
573                window.settlement_path
574            )));
575        }
576        if window.challenge_window_secs == 0 || window.recovery_window_secs == 0 {
577            return Err(Web3ContractError::InvalidBinding(format!(
578                "dispute window {:?} must have non-zero durations",
579                window.settlement_path
580            )));
581        }
582    }
583    if profile.finality_rules.is_empty() {
584        return Err(Web3ContractError::MissingField(
585            "web3_trust_profile.finality_rules",
586        ));
587    }
588    let mut seen_finality = HashSet::new();
589    for rule in &profile.finality_rules {
590        ensure_non_empty(&rule.chain_id, "web3_trust_profile.finality_rules.chain_id")?;
591        if rule.min_confirmations == 0 {
592            return Err(Web3ContractError::InvalidBinding(format!(
593                "finality rule {} must require at least one confirmation",
594                rule.chain_id
595            )));
596        }
597        if !seen_finality.insert(rule.chain_id.as_str()) {
598            return Err(Web3ContractError::DuplicateValue(rule.chain_id.clone()));
599        }
600    }
601    for chain_id in [&profile.primary_chain_id]
602        .into_iter()
603        .chain(profile.secondary_chain_ids.iter())
604    {
605        if !seen_finality.contains(chain_id.as_str()) {
606            return Err(Web3ContractError::UnknownReference(chain_id.clone()));
607        }
608    }
609
610    if profile.regulated_roles.is_empty() {
611        return Err(Web3ContractError::MissingField(
612            "web3_trust_profile.regulated_roles",
613        ));
614    }
615    let mut saw_custodian = false;
616    for role in &profile.regulated_roles {
617        ensure_non_empty(
618            &role.actor_id,
619            "web3_trust_profile.regulated_roles.actor_id",
620        )?;
621        ensure_non_empty(
622            &role.responsibility,
623            "web3_trust_profile.regulated_roles.responsibility",
624        )?;
625        if !role.custody_boundary_explicit {
626            return Err(Web3ContractError::InvalidBinding(format!(
627                "regulated role {:?} must keep custody boundary explicit",
628                role.role
629            )));
630        }
631        if role.role == Web3RegulatedRole::Custodian {
632            saw_custodian = true;
633        }
634    }
635    if !saw_custodian {
636        return Err(Web3ContractError::InvalidBinding(
637            "web3 trust profile must record at least one custodian role".to_string(),
638        ));
639    }
640
641    Ok(())
642}
643
644pub fn validate_web3_contract_package(
645    package: &Web3ContractPackage,
646) -> Result<(), Web3ContractError> {
647    if package.schema != CHIO_WEB3_CONTRACT_PACKAGE_SCHEMA {
648        return Err(Web3ContractError::UnsupportedSchema(package.schema.clone()));
649    }
650    ensure_non_empty(&package.package_id, "web3_contract_package.package_id")?;
651    ensure_non_empty(&package.version, "web3_contract_package.version")?;
652    ensure_non_empty(
653        &package.chio_contract_version,
654        "web3_contract_package.chio_contract_version",
655    )?;
656    if package.contracts.is_empty() {
657        return Err(Web3ContractError::MissingField(
658            "web3_contract_package.contracts",
659        ));
660    }
661    if package.bindings.is_empty() {
662        return Err(Web3ContractError::MissingField(
663            "web3_contract_package.bindings",
664        ));
665    }
666
667    let mut contract_ids = HashSet::new();
668    let mut contract_kinds = HashSet::new();
669    for contract in &package.contracts {
670        ensure_non_empty(
671            &contract.contract_id,
672            "web3_contract_package.contracts.contract_id",
673        )?;
674        ensure_non_empty(
675            &contract.interface_name,
676            "web3_contract_package.contracts.interface_name",
677        )?;
678        ensure_non_empty(
679            &contract.abi_reference,
680            "web3_contract_package.contracts.abi_reference",
681        )?;
682        ensure_non_empty(
683            &contract.implementation_reference,
684            "web3_contract_package.contracts.implementation_reference",
685        )?;
686        if !contract_ids.insert(contract.contract_id.as_str()) {
687            return Err(Web3ContractError::DuplicateValue(
688                contract.contract_id.clone(),
689            ));
690        }
691        if !contract_kinds.insert(contract.kind) {
692            return Err(Web3ContractError::DuplicateValue(format!(
693                "web3_contract_package.contract_kind:{:?}",
694                contract.kind
695            )));
696        }
697    }
698    for required in [
699        Web3ContractKind::RootRegistry,
700        Web3ContractKind::Escrow,
701        Web3ContractKind::BondVault,
702        Web3ContractKind::IdentityRegistry,
703        Web3ContractKind::PriceResolver,
704    ] {
705        if !contract_kinds.contains(&required) {
706            return Err(Web3ContractError::UnknownReference(format!(
707                "missing required contract kind {:?}",
708                required
709            )));
710        }
711    }
712
713    for binding in &package.bindings {
714        ensure_non_empty(
715            &binding.crate_path,
716            "web3_contract_package.bindings.crate_path",
717        )?;
718        ensure_non_empty(
719            &binding.module_name,
720            "web3_contract_package.bindings.module_name",
721        )?;
722        if binding.contract_ids.is_empty() {
723            return Err(Web3ContractError::MissingField(
724                "web3_contract_package.bindings.contract_ids",
725            ));
726        }
727        ensure_unique_strings(
728            &binding.contract_ids,
729            "web3_contract_package.bindings.contract_ids",
730        )?;
731        for contract_id in &binding.contract_ids {
732            if !contract_ids.contains(contract_id.as_str()) {
733                return Err(Web3ContractError::UnknownReference(contract_id.clone()));
734            }
735        }
736    }
737
738    ensure_unique_strings(
739        &package.deferred_capabilities,
740        "web3_contract_package.deferred_capabilities",
741    )?;
742
743    Ok(())
744}
745
746pub fn validate_web3_chain_configuration(
747    configuration: &Web3ChainConfiguration,
748) -> Result<(), Web3ContractError> {
749    if configuration.schema != CHIO_WEB3_CHAIN_CONFIGURATION_SCHEMA {
750        return Err(Web3ContractError::UnsupportedSchema(
751            configuration.schema.clone(),
752        ));
753    }
754    ensure_non_empty(
755        &configuration.package_id,
756        "web3_chain_configuration.package_id",
757    )?;
758    ensure_non_empty(
759        &configuration.primary_chain_id,
760        "web3_chain_configuration.primary_chain_id",
761    )?;
762    if configuration.deployments.is_empty() {
763        return Err(Web3ContractError::MissingField(
764            "web3_chain_configuration.deployments",
765        ));
766    }
767    if configuration.gas_profiles.is_empty() {
768        return Err(Web3ContractError::MissingField(
769            "web3_chain_configuration.gas_profiles",
770        ));
771    }
772
773    let mut deployment_ids = HashSet::new();
774    let mut primary_count = 0usize;
775    for deployment in &configuration.deployments {
776        ensure_non_empty(
777            &deployment.chain_id,
778            "web3_chain_configuration.deployments.chain_id",
779        )?;
780        ensure_non_empty(
781            &deployment.network_name,
782            "web3_chain_configuration.deployments.network_name",
783        )?;
784        ensure_non_empty(
785            &deployment.settlement_token_symbol,
786            "web3_chain_configuration.deployments.settlement_token_symbol",
787        )?;
788        for field in [
789            &deployment.settlement_token_address,
790            &deployment.root_registry_address,
791            &deployment.escrow_address,
792            &deployment.bond_vault_address,
793            &deployment.identity_registry_address,
794            &deployment.price_resolver_address,
795            &deployment.operator_address,
796        ] {
797            ensure_non_empty(field, "web3_chain_configuration.deployments.addresses")?;
798        }
799        if !deployment_ids.insert(deployment.chain_id.as_str()) {
800            return Err(Web3ContractError::DuplicateValue(
801                deployment.chain_id.clone(),
802            ));
803        }
804        if deployment.role == Web3ChainRole::Primary {
805            primary_count += 1;
806            if deployment.chain_id != configuration.primary_chain_id {
807                return Err(Web3ContractError::InvalidBinding(format!(
808                    "primary deployment {} does not match primary_chain_id {}",
809                    deployment.chain_id, configuration.primary_chain_id
810                )));
811            }
812        }
813    }
814    if primary_count != 1 {
815        return Err(Web3ContractError::InvalidBinding(
816            "web3 chain configuration must declare exactly one primary deployment".to_string(),
817        ));
818    }
819
820    let mut gas_profiles = HashSet::new();
821    for gas in &configuration.gas_profiles {
822        ensure_non_empty(
823            &gas.chain_id,
824            "web3_chain_configuration.gas_profiles.chain_id",
825        )?;
826        if !deployment_ids.contains(gas.chain_id.as_str()) {
827            return Err(Web3ContractError::UnknownReference(gas.chain_id.clone()));
828        }
829        if !gas_profiles.insert(gas.chain_id.as_str()) {
830            return Err(Web3ContractError::DuplicateValue(gas.chain_id.clone()));
831        }
832        for metric in [
833            gas.publish_root_gas,
834            gas.dual_sign_settlement_gas,
835            gas.merkle_settlement_gas,
836            gas.bond_release_gas,
837            gas.price_read_gas,
838        ] {
839            if metric == 0 {
840                return Err(Web3ContractError::InvalidBinding(format!(
841                    "gas profile {} must not contain zero-valued gas assumptions",
842                    gas.chain_id
843                )));
844            }
845        }
846    }
847
848    Ok(())
849}
850
851pub fn validate_oracle_conversion_evidence(
852    evidence: &OracleConversionEvidence,
853) -> Result<(), Web3ContractError> {
854    if evidence.schema != CHIO_ORACLE_CONVERSION_EVIDENCE_SCHEMA {
855        return Err(Web3ContractError::UnsupportedSchema(
856            evidence.schema.clone(),
857        ));
858    }
859    for field in [
860        &evidence.base,
861        &evidence.quote,
862        &evidence.authority,
863        &evidence.source,
864        &evidence.feed_address,
865        &evidence.original_currency,
866        &evidence.grant_currency,
867    ] {
868        ensure_non_empty(field, "oracle_conversion_evidence.field")?;
869    }
870    if evidence.authority != CHIO_LINK_ORACLE_AUTHORITY {
871        return Err(Web3ContractError::InvalidProof(format!(
872            "oracle conversion evidence authority {} is unsupported",
873            evidence.authority
874        )));
875    }
876    if evidence.rate_denominator == 0 {
877        return Err(Web3ContractError::InvalidProof(
878            "oracle conversion evidence rate_denominator must be non-zero".to_string(),
879        ));
880    }
881    if evidence.max_age_seconds == 0 {
882        return Err(Web3ContractError::InvalidProof(
883            "oracle conversion evidence max_age_seconds must be non-zero".to_string(),
884        ));
885    }
886    if evidence.original_cost_units == 0 || evidence.converted_cost_units == 0 {
887        return Err(Web3ContractError::InvalidProof(
888            "oracle conversion evidence cost units must be non-zero".to_string(),
889        ));
890    }
891    Ok(())
892}
893
894pub fn validate_anchor_inclusion_proof(
895    proof: &AnchorInclusionProof,
896) -> Result<(), Web3ContractError> {
897    if proof.schema != CHIO_ANCHOR_INCLUSION_PROOF_SCHEMA {
898        return Err(Web3ContractError::UnsupportedSchema(proof.schema.clone()));
899    }
900    validate_web3_identity_binding(&proof.key_binding_certificate)?;
901    if proof.receipt.id.trim().is_empty() {
902        return Err(Web3ContractError::MissingField(
903            "anchor_inclusion.receipt.id",
904        ));
905    }
906    if proof.receipt_inclusion.proof.tree_size == 0 {
907        return Err(Web3ContractError::InvalidProof(
908            "anchor inclusion proof tree_size must be non-zero".to_string(),
909        ));
910    }
911    if proof.receipt_inclusion.proof.leaf_index >= proof.receipt_inclusion.proof.tree_size {
912        return Err(Web3ContractError::InvalidProof(
913            "anchor inclusion proof leaf_index exceeds tree_size".to_string(),
914        ));
915    }
916    if proof.receipt_inclusion.checkpoint_seq != proof.checkpoint_statement.checkpoint_seq {
917        return Err(Web3ContractError::InvalidProof(
918            "receipt inclusion checkpoint_seq must match checkpoint statement".to_string(),
919        ));
920    }
921    if proof.receipt_inclusion.merkle_root != proof.checkpoint_statement.merkle_root {
922        return Err(Web3ContractError::InvalidProof(
923            "receipt inclusion merkle_root must match checkpoint statement".to_string(),
924        ));
925    }
926    if proof.checkpoint_statement.schema != CHIO_CHECKPOINT_STATEMENT_SCHEMA {
927        return Err(Web3ContractError::UnsupportedSchema(
928            proof.checkpoint_statement.schema.clone(),
929        ));
930    }
931    if proof.checkpoint_statement.batch_start_seq > proof.checkpoint_statement.batch_end_seq {
932        return Err(Web3ContractError::InvalidProof(
933            "checkpoint statement batch_start_seq must be <= batch_end_seq".to_string(),
934        ));
935    }
936    if proof.checkpoint_statement.tree_size == 0 {
937        return Err(Web3ContractError::InvalidProof(
938            "checkpoint statement tree_size must be non-zero".to_string(),
939        ));
940    }
941    if proof.checkpoint_statement.tree_size as usize != proof.receipt_inclusion.proof.tree_size {
942        return Err(Web3ContractError::InvalidProof(
943            "checkpoint statement tree_size must match receipt inclusion proof".to_string(),
944        ));
945    }
946    if let Some(chain_anchor) = proof.chain_anchor.as_ref() {
947        ensure_non_empty(
948            &chain_anchor.chain_id,
949            "anchor_inclusion.chain_anchor.chain_id",
950        )?;
951        ensure_non_empty(
952            &chain_anchor.contract_address,
953            "anchor_inclusion.chain_anchor.contract_address",
954        )?;
955        ensure_non_empty(
956            &chain_anchor.operator_address,
957            "anchor_inclusion.chain_anchor.operator_address",
958        )?;
959        ensure_non_empty(
960            &chain_anchor.tx_hash,
961            "anchor_inclusion.chain_anchor.tx_hash",
962        )?;
963        ensure_non_empty(
964            &chain_anchor.block_hash,
965            "anchor_inclusion.chain_anchor.block_hash",
966        )?;
967        if chain_anchor.anchored_checkpoint_seq != proof.checkpoint_statement.checkpoint_seq {
968            return Err(Web3ContractError::InvalidProof(
969                "chain anchor checkpoint seq must match checkpoint statement".to_string(),
970            ));
971        }
972        if chain_anchor.anchored_merkle_root != proof.checkpoint_statement.merkle_root {
973            return Err(Web3ContractError::InvalidProof(
974                "chain anchor root must match checkpoint statement".to_string(),
975            ));
976        }
977        if chain_anchor.operator_address
978            != proof.key_binding_certificate.certificate.settlement_address
979        {
980            return Err(Web3ContractError::InvalidBinding(
981                "chain anchor operator address must match settlement binding".to_string(),
982            ));
983        }
984        if !proof
985            .key_binding_certificate
986            .certificate
987            .purpose
988            .contains(&Web3KeyBindingPurpose::Anchor)
989        {
990            return Err(Web3ContractError::InvalidBinding(
991                "anchor proof requires a binding certificate scoped to anchor".to_string(),
992            ));
993        }
994        if !proof
995            .key_binding_certificate
996            .certificate
997            .chain_scope
998            .iter()
999            .any(|chain_id| chain_id == &chain_anchor.chain_id)
1000        {
1001            return Err(Web3ContractError::InvalidBinding(format!(
1002                "binding certificate does not cover anchor chain {}",
1003                chain_anchor.chain_id
1004            )));
1005        }
1006    }
1007    if let Some(bitcoin_anchor) = proof.bitcoin_anchor.as_ref() {
1008        ensure_non_empty(
1009            &bitcoin_anchor.method,
1010            "anchor_inclusion.bitcoin_anchor.method",
1011        )?;
1012        ensure_non_empty(
1013            &bitcoin_anchor.ots_proof_b64,
1014            "anchor_inclusion.bitcoin_anchor.ots_proof_b64",
1015        )?;
1016        ensure_non_empty(
1017            &bitcoin_anchor.bitcoin_block_hash,
1018            "anchor_inclusion.bitcoin_anchor.bitcoin_block_hash",
1019        )?;
1020        if proof.super_root_inclusion.is_none() {
1021            return Err(Web3ContractError::InvalidProof(
1022                "Bitcoin anchor requires super-root inclusion metadata".to_string(),
1023            ));
1024        }
1025    }
1026    if let Some(super_root) = proof.super_root_inclusion.as_ref() {
1027        if super_root.aggregated_checkpoint_start > super_root.aggregated_checkpoint_end {
1028            return Err(Web3ContractError::InvalidProof(
1029                "super-root inclusion checkpoint range must be ordered".to_string(),
1030            ));
1031        }
1032        if proof.checkpoint_statement.checkpoint_seq < super_root.aggregated_checkpoint_start
1033            || proof.checkpoint_statement.checkpoint_seq > super_root.aggregated_checkpoint_end
1034        {
1035            return Err(Web3ContractError::InvalidProof(
1036                "checkpoint statement must fall within the super-root aggregation range"
1037                    .to_string(),
1038            ));
1039        }
1040        if super_root.proof.tree_size == 0 {
1041            return Err(Web3ContractError::InvalidProof(
1042                "super-root inclusion tree_size must be non-zero".to_string(),
1043            ));
1044        }
1045    }
1046    Ok(())
1047}
1048
1049pub fn verify_checkpoint_statement(
1050    statement: &Web3CheckpointStatement,
1051) -> Result<(), Web3ContractError> {
1052    let body = checkpoint_statement_body(statement);
1053    let verified = statement
1054        .kernel_key
1055        .verify_canonical(&body, &statement.signature)
1056        .map_err(|error| Web3ContractError::InvalidProof(error.to_string()))?;
1057    if !verified {
1058        return Err(Web3ContractError::InvalidProof(
1059            "checkpoint statement signature verification failed".to_string(),
1060        ));
1061    }
1062    Ok(())
1063}
1064
1065pub fn verify_anchor_inclusion_proof(
1066    proof: &AnchorInclusionProof,
1067) -> Result<(), Web3ContractError> {
1068    validate_anchor_inclusion_proof(proof)?;
1069    let receipt_verified = proof
1070        .receipt
1071        .verify_signature()
1072        .map_err(|error| Web3ContractError::InvalidProof(error.to_string()))?;
1073    if !receipt_verified {
1074        return Err(Web3ContractError::InvalidProof(
1075            "receipt signature verification failed".to_string(),
1076        ));
1077    }
1078    verify_web3_identity_binding(&proof.key_binding_certificate)?;
1079    verify_checkpoint_statement(&proof.checkpoint_statement)?;
1080    if proof.key_binding_certificate.certificate.chio_public_key != proof.receipt.kernel_key {
1081        return Err(Web3ContractError::InvalidBinding(
1082            "binding certificate public key must match receipt kernel_key".to_string(),
1083        ));
1084    }
1085    if proof.checkpoint_statement.kernel_key != proof.receipt.kernel_key {
1086        return Err(Web3ContractError::InvalidProof(
1087            "checkpoint statement kernel_key must match receipt kernel_key".to_string(),
1088        ));
1089    }
1090
1091    let receipt_body = proof.receipt.body();
1092    let receipt_bytes = canonical_json_bytes(&receipt_body)
1093        .map_err(|error| Web3ContractError::InvalidProof(error.to_string()))?;
1094    let receipt_leaf = leaf_hash(&receipt_bytes);
1095    if !proof
1096        .receipt_inclusion
1097        .proof
1098        .verify_hash(receipt_leaf, &proof.receipt_inclusion.merkle_root)
1099    {
1100        return Err(Web3ContractError::InvalidProof(
1101            "receipt inclusion Merkle proof verification failed".to_string(),
1102        ));
1103    }
1104    if let Some(super_root) = proof.super_root_inclusion.as_ref() {
1105        if !super_root
1106            .proof
1107            .verify_hash(proof.receipt_inclusion.merkle_root, &super_root.super_root)
1108        {
1109            return Err(Web3ContractError::InvalidProof(
1110                "super-root inclusion verification failed".to_string(),
1111            ));
1112        }
1113    }
1114    Ok(())
1115}
1116
1117pub fn validate_web3_settlement_dispatch(
1118    dispatch: &Web3SettlementDispatchArtifact,
1119) -> Result<(), Web3ContractError> {
1120    if dispatch.schema != CHIO_WEB3_SETTLEMENT_DISPATCH_SCHEMA {
1121        return Err(Web3ContractError::UnsupportedSchema(
1122            dispatch.schema.clone(),
1123        ));
1124    }
1125    for field in [
1126        &dispatch.dispatch_id,
1127        &dispatch.trust_profile_id,
1128        &dispatch.contract_package_id,
1129        &dispatch.chain_id,
1130        &dispatch.escrow_id,
1131        &dispatch.escrow_contract,
1132        &dispatch.bond_vault_contract,
1133        &dispatch.beneficiary_address,
1134    ] {
1135        ensure_non_empty(field, "web3_settlement_dispatch.field")?;
1136    }
1137    ensure_money(
1138        &dispatch.settlement_amount,
1139        "web3_settlement_dispatch.settlement_amount",
1140    )?;
1141    if !dispatch.support_boundary.real_dispatch_supported {
1142        return Err(Web3ContractError::InvalidSettlement(
1143            "web3 settlement dispatch must explicitly mark real dispatch as supported".to_string(),
1144        ));
1145    }
1146    if !dispatch.support_boundary.custody_boundary_explicit {
1147        return Err(Web3ContractError::InvalidSettlement(
1148            "web3 settlement dispatch must keep custody boundaries explicit".to_string(),
1149        ));
1150    }
1151    if dispatch.settlement_path == Web3SettlementPath::MerkleProof
1152        && !dispatch.support_boundary.anchor_proof_required
1153    {
1154        return Err(Web3ContractError::InvalidSettlement(
1155            "Merkle-proof settlement dispatch must require anchor proof reconciliation".to_string(),
1156        ));
1157    }
1158    if dispatch.capital_instruction.body.schema != CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA {
1159        return Err(Web3ContractError::UnsupportedSchema(
1160            dispatch.capital_instruction.body.schema.clone(),
1161        ));
1162    }
1163    if dispatch.capital_instruction.body.action
1164        == CapitalExecutionInstructionAction::CancelInstruction
1165    {
1166        return Err(Web3ContractError::InvalidSettlement(
1167            "web3 settlement dispatch cannot use cancel_instruction as the primary action"
1168                .to_string(),
1169        ));
1170    }
1171    if dispatch.capital_instruction.body.rail.kind != CapitalExecutionRailKind::Web3 {
1172        return Err(Web3ContractError::InvalidSettlement(
1173            "web3 settlement dispatch requires capital_instruction rail.kind = web3".to_string(),
1174        ));
1175    }
1176    let Some(amount) = dispatch.capital_instruction.body.amount.as_ref() else {
1177        return Err(Web3ContractError::MissingField(
1178            "web3_settlement_dispatch.capital_instruction.amount",
1179        ));
1180    };
1181    if amount != &dispatch.settlement_amount {
1182        return Err(Web3ContractError::InvalidSettlement(
1183            "web3 settlement dispatch settlement_amount must match capital_instruction amount"
1184                .to_string(),
1185        ));
1186    }
1187    if dispatch.capital_instruction.body.reconciled_state
1188        != CapitalExecutionReconciledState::NotObserved
1189    {
1190        return Err(Web3ContractError::InvalidSettlement(
1191            "web3 settlement dispatch capital_instruction must remain unreconciled until execution receipt"
1192                .to_string(),
1193        ));
1194    }
1195    validate_transfer_completion_flow_binding(&dispatch.capital_instruction.body)?;
1196    if let Some(bond) = dispatch.bond.as_ref() {
1197        if bond.body.lifecycle_state != CreditBondLifecycleState::Active {
1198            return Err(Web3ContractError::InvalidSettlement(
1199                "web3 settlement dispatch requires an active bond when bond backing is present"
1200                    .to_string(),
1201            ));
1202        }
1203    }
1204    Ok(())
1205}
1206
1207pub fn validate_web3_settlement_execution_receipt(
1208    receipt: &Web3SettlementExecutionReceiptArtifact,
1209) -> Result<(), Web3ContractError> {
1210    if receipt.schema != CHIO_WEB3_SETTLEMENT_RECEIPT_SCHEMA {
1211        return Err(Web3ContractError::UnsupportedSchema(receipt.schema.clone()));
1212    }
1213    ensure_non_empty(
1214        &receipt.execution_receipt_id,
1215        "web3_settlement_receipt.execution_receipt_id",
1216    )?;
1217    ensure_non_empty(
1218        &receipt.settlement_reference,
1219        "web3_settlement_receipt.settlement_reference",
1220    )?;
1221    validate_web3_settlement_dispatch(&receipt.dispatch)?;
1222    ensure_money(
1223        &receipt.observed_execution.amount,
1224        "web3_settlement_receipt.observed_amount",
1225    )?;
1226    ensure_money(
1227        &receipt.settled_amount,
1228        "web3_settlement_receipt.settled_amount",
1229    )?;
1230    if receipt.observed_execution.amount.currency != receipt.dispatch.settlement_amount.currency {
1231        return Err(Web3ContractError::InvalidSettlement(
1232            "observed execution currency must match dispatch settlement currency".to_string(),
1233        ));
1234    }
1235    if receipt.settled_amount.currency != receipt.dispatch.settlement_amount.currency {
1236        return Err(Web3ContractError::InvalidSettlement(
1237            "settled amount currency must match dispatch settlement currency".to_string(),
1238        ));
1239    }
1240    if receipt.observed_execution.amount != receipt.settled_amount {
1241        return Err(Web3ContractError::InvalidSettlement(
1242            "observed execution amount must equal settled_amount".to_string(),
1243        ));
1244    }
1245    if let Some(anchor_proof) = receipt.reconciled_anchor_proof.as_ref() {
1246        validate_anchor_inclusion_proof(anchor_proof)?;
1247        if let Some(chain_anchor) = anchor_proof.chain_anchor.as_ref() {
1248            if chain_anchor.chain_id != receipt.dispatch.chain_id {
1249                return Err(Web3ContractError::InvalidSettlement(
1250                    "anchor proof chain_id must match settlement dispatch chain_id".to_string(),
1251                ));
1252            }
1253        }
1254    }
1255    if let Some(oracle_evidence) = receipt.oracle_evidence.as_ref() {
1256        validate_oracle_conversion_evidence(oracle_evidence)?;
1257    }
1258    if receipt
1259        .dispatch
1260        .support_boundary
1261        .oracle_evidence_required_for_fx
1262        && !matches!(
1263            receipt.lifecycle_state,
1264            Web3SettlementLifecycleState::TimedOut
1265                | Web3SettlementLifecycleState::Failed
1266                | Web3SettlementLifecycleState::Reorged
1267        )
1268        && receipt.oracle_evidence.is_none()
1269    {
1270        return Err(Web3ContractError::InvalidSettlement(
1271            "receipt requires oracle_evidence for FX-sensitive settlement paths".to_string(),
1272        ));
1273    }
1274
1275    match receipt.lifecycle_state {
1276        Web3SettlementLifecycleState::PendingDispatch
1277        | Web3SettlementLifecycleState::EscrowLocked => {
1278            return Err(Web3ContractError::InvalidSettlement(
1279                "execution receipts must record an observed terminal or reconciled lifecycle state"
1280                    .to_string(),
1281            ));
1282        }
1283        Web3SettlementLifecycleState::PartiallySettled => {
1284            if receipt.settled_amount.units == 0
1285                || receipt.settled_amount.units >= receipt.dispatch.settlement_amount.units
1286            {
1287                return Err(Web3ContractError::InvalidSettlement(
1288                    "partially_settled receipts must settle a non-zero amount smaller than the dispatch amount"
1289                        .to_string(),
1290                ));
1291            }
1292        }
1293        Web3SettlementLifecycleState::Settled => {
1294            if receipt.settled_amount != receipt.dispatch.settlement_amount {
1295                return Err(Web3ContractError::InvalidSettlement(
1296                    "settled receipts must match the dispatch settlement amount".to_string(),
1297                ));
1298            }
1299        }
1300        Web3SettlementLifecycleState::Reversed | Web3SettlementLifecycleState::ChargedBack => {
1301            ensure_non_empty(
1302                receipt.reversal_of.as_deref().unwrap_or_default(),
1303                "web3_settlement_receipt.reversal_of",
1304            )?;
1305            if !receipt.dispatch.support_boundary.reversal_supported {
1306                return Err(Web3ContractError::InvalidSettlement(
1307                    "receipt records reversal state but dispatch did not declare reversal support"
1308                        .to_string(),
1309                ));
1310            }
1311        }
1312        Web3SettlementLifecycleState::TimedOut
1313        | Web3SettlementLifecycleState::Failed
1314        | Web3SettlementLifecycleState::Reorged => {
1315            ensure_non_empty(
1316                receipt.failure_reason.as_deref().unwrap_or_default(),
1317                "web3_settlement_receipt.failure_reason",
1318            )?;
1319        }
1320    }
1321
1322    let must_have_anchor = receipt.dispatch.support_boundary.anchor_proof_required
1323        && !matches!(
1324            receipt.lifecycle_state,
1325            Web3SettlementLifecycleState::TimedOut | Web3SettlementLifecycleState::Failed
1326        );
1327    if must_have_anchor && receipt.reconciled_anchor_proof.is_none() {
1328        return Err(Web3ContractError::InvalidSettlement(
1329            "receipt requires reconciled anchor proof for the selected settlement path".to_string(),
1330        ));
1331    }
1332
1333    Ok(())
1334}
1335
1336pub fn validate_web3_qualification_matrix(
1337    matrix: &Web3QualificationMatrix,
1338) -> Result<(), Web3ContractError> {
1339    if matrix.schema != CHIO_WEB3_QUALIFICATION_MATRIX_SCHEMA {
1340        return Err(Web3ContractError::UnsupportedSchema(matrix.schema.clone()));
1341    }
1342    ensure_non_empty(
1343        &matrix.trust_profile_id,
1344        "web3_qualification_matrix.trust_profile_id",
1345    )?;
1346    ensure_non_empty(
1347        &matrix.contract_package_id,
1348        "web3_qualification_matrix.contract_package_id",
1349    )?;
1350    if matrix.cases.is_empty() {
1351        return Err(Web3ContractError::MissingField(
1352            "web3_qualification_matrix.cases",
1353        ));
1354    }
1355    let mut case_ids = HashSet::new();
1356    for case in &matrix.cases {
1357        ensure_non_empty(&case.id, "web3_qualification_matrix.case.id")?;
1358        ensure_non_empty(&case.name, "web3_qualification_matrix.case.name")?;
1359        ensure_non_empty(&case.notes, "web3_qualification_matrix.case.notes")?;
1360        if !case_ids.insert(case.id.as_str()) {
1361            return Err(Web3ContractError::DuplicateValue(case.id.clone()));
1362        }
1363        if case.requirement_ids.is_empty() {
1364            return Err(Web3ContractError::InvalidQualificationCase(format!(
1365                "case {} must cite at least one requirement id",
1366                case.id
1367            )));
1368        }
1369        ensure_unique_strings(
1370            &case.requirement_ids,
1371            "web3_qualification_matrix.case.requirement_ids",
1372        )?;
1373    }
1374    Ok(())
1375}
1376
1377#[derive(Debug, Clone, Serialize)]
1378struct Web3CheckpointStatementBody {
1379    schema: String,
1380    checkpoint_seq: u64,
1381    batch_start_seq: u64,
1382    batch_end_seq: u64,
1383    tree_size: u64,
1384    merkle_root: Hash,
1385    issued_at: u64,
1386    #[serde(default, skip_serializing_if = "Option::is_none")]
1387    previous_checkpoint_sha256: Option<String>,
1388    kernel_key: PublicKey,
1389}
1390
1391fn checkpoint_statement_body(statement: &Web3CheckpointStatement) -> Web3CheckpointStatementBody {
1392    Web3CheckpointStatementBody {
1393        schema: statement.schema.clone(),
1394        checkpoint_seq: statement.checkpoint_seq,
1395        batch_start_seq: statement.batch_start_seq,
1396        batch_end_seq: statement.batch_end_seq,
1397        tree_size: statement.tree_size,
1398        merkle_root: statement.merkle_root,
1399        issued_at: statement.issued_at,
1400        previous_checkpoint_sha256: statement.previous_checkpoint_sha256.clone(),
1401        kernel_key: statement.kernel_key.clone(),
1402    }
1403}
1404
1405fn ensure_non_empty(value: &str, field: &'static str) -> Result<(), Web3ContractError> {
1406    if value.trim().is_empty() {
1407        Err(Web3ContractError::MissingField(field))
1408    } else {
1409        Ok(())
1410    }
1411}
1412
1413fn validate_transfer_completion_flow_binding(
1414    instruction: &crate::credit::CapitalExecutionInstructionArtifact,
1415) -> Result<(), Web3ContractError> {
1416    if instruction.action != CapitalExecutionInstructionAction::TransferFunds {
1417        return Ok(());
1418    }
1419    let governed_receipt_id =
1420        instruction
1421            .governed_receipt_id
1422            .as_deref()
1423            .ok_or(Web3ContractError::MissingField(
1424                "web3_settlement_dispatch.capital_instruction.governed_receipt_id",
1425            ))?;
1426    ensure_non_empty(
1427        governed_receipt_id,
1428        "web3_settlement_dispatch.capital_instruction.governed_receipt_id",
1429    )?;
1430    let completion_flow_row_id =
1431        instruction
1432            .completion_flow_row_id
1433            .as_deref()
1434            .ok_or(Web3ContractError::MissingField(
1435                "web3_settlement_dispatch.capital_instruction.completion_flow_row_id",
1436            ))?;
1437    ensure_non_empty(
1438        completion_flow_row_id,
1439        "web3_settlement_dispatch.capital_instruction.completion_flow_row_id",
1440    )?;
1441    let expected_row_id = format!("economic-completion-flow:{governed_receipt_id}");
1442    if completion_flow_row_id != expected_row_id {
1443        return Err(Web3ContractError::InvalidSettlement(
1444            "web3 settlement dispatch completion_flow_row_id must match governed_receipt_id"
1445                .to_string(),
1446        ));
1447    }
1448    Ok(())
1449}
1450
1451fn ensure_unique_strings(values: &[String], field: &'static str) -> Result<(), Web3ContractError> {
1452    let mut seen = HashSet::new();
1453    for value in values {
1454        ensure_non_empty(value, field)?;
1455        if !seen.insert(value.as_str()) {
1456            return Err(Web3ContractError::DuplicateValue(value.clone()));
1457        }
1458    }
1459    Ok(())
1460}
1461
1462fn ensure_unique_copy_values<T>(values: &[T], field: &'static str) -> Result<(), Web3ContractError>
1463where
1464    T: Eq + std::hash::Hash + Copy + std::fmt::Debug,
1465{
1466    let mut seen = HashSet::new();
1467    for value in values {
1468        if !seen.insert(*value) {
1469            return Err(Web3ContractError::DuplicateValue(format!(
1470                "{field}:{value:?}"
1471            )));
1472        }
1473    }
1474    Ok(())
1475}
1476
1477fn ensure_money(amount: &MonetaryAmount, field: &'static str) -> Result<(), Web3ContractError> {
1478    if amount.units == 0 {
1479        return Err(Web3ContractError::InvalidSettlement(format!(
1480            "{field} must be non-zero"
1481        )));
1482    }
1483    ensure_non_empty(&amount.currency, field)
1484        .map_err(|_| Web3ContractError::InvalidSettlement(format!("{field} currency is required")))
1485}
1486
1487#[cfg(test)]
1488#[allow(clippy::unwrap_used, clippy::expect_used)]
1489mod tests {
1490    use serde_json::json;
1491
1492    use super::*;
1493    use crate::capability::MonetaryAmount;
1494    use crate::credit::{
1495        CapitalBookEvidenceKind, CapitalBookEvidenceReference, CapitalBookQuery,
1496        CapitalBookSourceKind, CapitalExecutionAuthorityStep, CapitalExecutionInstructionAction,
1497        CapitalExecutionInstructionSupportBoundary, CapitalExecutionIntendedState,
1498        CapitalExecutionObservation, CapitalExecutionRail, CapitalExecutionRole,
1499        CapitalExecutionWindow, SignedCapitalExecutionInstruction,
1500        CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA,
1501    };
1502    use crate::crypto::{sha256_hex, Keypair};
1503    use crate::merkle::MerkleTree;
1504    use crate::receipt::{ChioReceipt, ChioReceiptBody, Decision, ToolCallAction};
1505
1506    fn operator_keypair() -> Keypair {
1507        Keypair::from_seed(&[7u8; 32])
1508    }
1509
1510    fn treasury_keypair() -> Keypair {
1511        Keypair::from_seed(&[9u8; 32])
1512    }
1513
1514    fn sample_binding() -> SignedWeb3IdentityBinding {
1515        let operator = operator_keypair();
1516        let certificate = Web3IdentityBindingCertificate {
1517            schema: CHIO_KEY_BINDING_CERTIFICATE_SCHEMA.to_string(),
1518            chio_identity: format!("did:chio:{}", operator.public_key().to_hex()),
1519            chio_public_key: operator.public_key(),
1520            chain_scope: vec!["eip155:8453".to_string(), "eip155:42161".to_string()],
1521            purpose: vec![Web3KeyBindingPurpose::Anchor, Web3KeyBindingPurpose::Settle],
1522            settlement_address: "0x1111111111111111111111111111111111111111".to_string(),
1523            issued_at: 1_743_292_800,
1524            expires_at: 1_774_828_800,
1525            nonce: "0123456789abcdef0123456789abcdef".to_string(),
1526        };
1527        let (signature, _) = operator.sign_canonical(&certificate).unwrap();
1528        SignedWeb3IdentityBinding {
1529            certificate,
1530            signature,
1531        }
1532    }
1533
1534    fn sample_trust_profile() -> Web3TrustProfile {
1535        Web3TrustProfile {
1536            schema: CHIO_WEB3_TRUST_PROFILE_SCHEMA.to_string(),
1537            profile_id: "chio.official-web3-stack".to_string(),
1538            chio_contract_version: "2.0".to_string(),
1539            primary_chain_id: "eip155:8453".to_string(),
1540            secondary_chain_ids: vec!["eip155:42161".to_string()],
1541            operator_binding: sample_binding(),
1542            proof_bundle_required: true,
1543            dispute_windows: vec![
1544                Web3DisputeWindow {
1545                    settlement_path: Web3SettlementPath::DualSignature,
1546                    challenge_window_secs: 600,
1547                    recovery_window_secs: 3_600,
1548                    dispute_policy: Web3DisputePolicy::OffChainArbitration,
1549                },
1550                Web3DisputeWindow {
1551                    settlement_path: Web3SettlementPath::MerkleProof,
1552                    challenge_window_secs: 900,
1553                    recovery_window_secs: 86_400,
1554                    dispute_policy: Web3DisputePolicy::TimeoutRefund,
1555                },
1556            ],
1557            finality_rules: vec![
1558                Web3ChainFinalityRule {
1559                    chain_id: "eip155:8453".to_string(),
1560                    mode: Web3FinalityMode::OptimisticL2,
1561                    min_confirmations: 20,
1562                },
1563                Web3ChainFinalityRule {
1564                    chain_id: "eip155:42161".to_string(),
1565                    mode: Web3FinalityMode::L1Finalized,
1566                    min_confirmations: 12,
1567                },
1568            ],
1569            regulated_roles: vec![
1570                Web3RegulatedRoleAssumption {
1571                    role: Web3RegulatedRole::Operator,
1572                    actor_id: "chio-operator-main".to_string(),
1573                    responsibility: "Originates governed dispatch and maintains local policy activation."
1574                        .to_string(),
1575                    custody_boundary_explicit: true,
1576                },
1577                Web3RegulatedRoleAssumption {
1578                    role: Web3RegulatedRole::Custodian,
1579                    actor_id: "custodian-base-main".to_string(),
1580                    responsibility: "Holds settlement-side keys and custody accounts for the official stack."
1581                        .to_string(),
1582                    custody_boundary_explicit: true,
1583                },
1584                Web3RegulatedRoleAssumption {
1585                    role: Web3RegulatedRole::Arbitrator,
1586                    actor_id: "settlement-dispute-panel".to_string(),
1587                    responsibility: "Handles off-chain challenge and reversal review during dispute windows."
1588                        .to_string(),
1589                    custody_boundary_explicit: true,
1590                },
1591            ],
1592            custody_boundary_note:
1593                "Chio governs intent, proofs, and policy admission; custodians and payment institutions remain explicit operators of record."
1594                    .to_string(),
1595            local_policy_activation_required: true,
1596        }
1597    }
1598
1599    fn sample_oracle_evidence() -> OracleConversionEvidence {
1600        OracleConversionEvidence {
1601            schema: CHIO_ORACLE_CONVERSION_EVIDENCE_SCHEMA.to_string(),
1602            base: "ETH".to_string(),
1603            quote: "USD".to_string(),
1604            authority: CHIO_LINK_ORACLE_AUTHORITY.to_string(),
1605            rate_numerator: 300_000,
1606            rate_denominator: 100,
1607            source: "chainlink".to_string(),
1608            feed_address: "0x639Fe6ab55C921f74e7fac1ee960C0B6293ba612".to_string(),
1609            updated_at: 1_743_292_740,
1610            max_age_seconds: 3_600,
1611            cache_age_seconds: 45,
1612            converted_cost_units: 300,
1613            original_cost_units: 100_000_000_000_000,
1614            original_currency: "ETH".to_string(),
1615            grant_currency: "USD".to_string(),
1616        }
1617    }
1618
1619    fn sample_receipt() -> ChioReceipt {
1620        let operator = operator_keypair();
1621        let parameters = json!({
1622            "to": "0x2222222222222222222222222222222222222222",
1623            "amount": 150,
1624            "currency": "USDC"
1625        });
1626        let action = ToolCallAction::from_parameters(parameters).unwrap();
1627        let body = ChioReceiptBody {
1628            id: "rcpt-web3-1".to_string(),
1629            timestamp: 1_743_292_800,
1630            capability_id: "cap-web3-1".to_string(),
1631            tool_server: "chio-settle".to_string(),
1632            tool_name: "release_escrow".to_string(),
1633            action,
1634            decision: Decision::Allow,
1635            content_hash: sha256_hex(b"web3-settlement"),
1636            policy_hash: sha256_hex(b"policy-web3"),
1637            evidence: vec![],
1638            metadata: Some(json!({
1639                "financial": {
1640                    "grant_index": 0,
1641                    "cost_charged": 150,
1642                    "currency": "USD",
1643                    "budget_remaining": 850,
1644                    "budget_total": 1000,
1645                    "delegation_depth": 1,
1646                    "root_budget_holder": "subject-1",
1647                    "payment_reference": "escrow-1",
1648                    "settlement_status": "pending",
1649                    "oracle_evidence": sample_oracle_evidence()
1650                }
1651            })),
1652            trust_level: chio_core_types::receipt::TrustLevel::default(),
1653            tenant_id: None,
1654            kernel_key: operator.public_key(),
1655        };
1656        ChioReceipt::sign(body, &operator).unwrap()
1657    }
1658
1659    fn sample_anchor_inclusion_proof() -> AnchorInclusionProof {
1660        let operator = operator_keypair();
1661        let receipt = sample_receipt();
1662        let receipt_body = receipt.body();
1663        let receipt_bytes = canonical_json_bytes(&receipt_body).unwrap();
1664        let tree = MerkleTree::from_leaves(&[receipt_bytes]).unwrap();
1665        let merkle_root = tree.root();
1666        let inclusion = Web3ReceiptInclusion {
1667            checkpoint_seq: 1_042,
1668            merkle_root,
1669            proof: tree.inclusion_proof(0).unwrap(),
1670        };
1671        let mut statement = Web3CheckpointStatement {
1672            schema: CHIO_CHECKPOINT_STATEMENT_SCHEMA.to_string(),
1673            checkpoint_seq: 1_042,
1674            batch_start_seq: 104_101,
1675            batch_end_seq: 104_200,
1676            tree_size: 1,
1677            merkle_root,
1678            issued_at: 1_743_292_800,
1679            previous_checkpoint_sha256: None,
1680            kernel_key: operator.public_key(),
1681            signature: Signature::from_hex(
1682                "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
1683            )
1684            .unwrap(),
1685        };
1686        let body = checkpoint_statement_body(&statement);
1687        let (signature, _) = operator.sign_canonical(&body).unwrap();
1688        statement.signature = signature;
1689
1690        AnchorInclusionProof {
1691            schema: CHIO_ANCHOR_INCLUSION_PROOF_SCHEMA.to_string(),
1692            receipt,
1693            receipt_inclusion: inclusion,
1694            checkpoint_statement: statement,
1695            chain_anchor: Some(Web3ChainAnchorRecord {
1696                chain_id: "eip155:8453".to_string(),
1697                contract_address: "0x1000000000000000000000000000000000000001".to_string(),
1698                operator_address: "0x1111111111111111111111111111111111111111".to_string(),
1699                tx_hash: "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1700                    .to_string(),
1701                block_number: 12_345_678,
1702                block_hash: "0xbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1703                    .to_string(),
1704                anchored_merkle_root: merkle_root,
1705                anchored_checkpoint_seq: 1_042,
1706            }),
1707            bitcoin_anchor: None,
1708            super_root_inclusion: None,
1709            key_binding_certificate: sample_binding(),
1710        }
1711    }
1712
1713    fn sample_capital_instruction() -> SignedCapitalExecutionInstruction {
1714        let signer = treasury_keypair();
1715        SignedCapitalExecutionInstruction::sign(
1716            crate::credit::CapitalExecutionInstructionArtifact {
1717                schema: CAPITAL_EXECUTION_INSTRUCTION_ARTIFACT_SCHEMA.to_string(),
1718                instruction_id: "cei-web3-1".to_string(),
1719                issued_at: 1_743_292_800,
1720                query: CapitalBookQuery {
1721                    agent_subject: Some("subject-1".to_string()),
1722                    ..CapitalBookQuery::default()
1723                },
1724                subject_key: "subject-1".to_string(),
1725                source_id: "capital-source:facility:facility-1".to_string(),
1726                source_kind: CapitalBookSourceKind::FacilityCommitment,
1727                governed_receipt_id: Some("rcpt-web3-1".to_string()),
1728                completion_flow_row_id: Some("economic-completion-flow:rcpt-web3-1".to_string()),
1729                action: CapitalExecutionInstructionAction::TransferFunds,
1730                owner_role: CapitalExecutionRole::OperatorTreasury,
1731                counterparty_role: CapitalExecutionRole::AgentCounterparty,
1732                counterparty_id: "subject-1".to_string(),
1733                amount: Some(MonetaryAmount {
1734                    units: 150,
1735                    currency: "USD".to_string(),
1736                }),
1737                authority_chain: vec![
1738                    CapitalExecutionAuthorityStep {
1739                        role: CapitalExecutionRole::OperatorTreasury,
1740                        principal_id: "treasury-1".to_string(),
1741                        approved_at: 1_743_292_790,
1742                        expires_at: 1_743_293_800,
1743                        note: Some("governed release".to_string()),
1744                    },
1745                    CapitalExecutionAuthorityStep {
1746                        role: CapitalExecutionRole::Custodian,
1747                        principal_id: "custodian-base-main".to_string(),
1748                        approved_at: 1_743_292_795,
1749                        expires_at: 1_743_293_800,
1750                        note: Some("official web3 stack".to_string()),
1751                    },
1752                ],
1753                execution_window: CapitalExecutionWindow {
1754                    not_before: 1_743_292_800,
1755                    not_after: 1_743_293_800,
1756                },
1757                rail: CapitalExecutionRail {
1758                    kind: CapitalExecutionRailKind::Web3,
1759                    rail_id: "base-mainnet-usdc".to_string(),
1760                    custody_provider_id: "custodian-base-main".to_string(),
1761                    source_account_ref: Some("vault:facility-main".to_string()),
1762                    destination_account_ref: Some(
1763                        "0x2222222222222222222222222222222222222222".to_string(),
1764                    ),
1765                    jurisdiction: Some("US".to_string()),
1766                },
1767                intended_state: CapitalExecutionIntendedState::PendingExecution,
1768                reconciled_state: CapitalExecutionReconciledState::NotObserved,
1769                related_instruction_id: None,
1770                observed_execution: None,
1771                support_boundary: CapitalExecutionInstructionSupportBoundary {
1772                    capital_book_authoritative: true,
1773                    external_execution_authoritative: false,
1774                    automatic_dispatch_supported: true,
1775                    custody_neutral_instruction_supported: false,
1776                },
1777                evidence_refs: vec![CapitalBookEvidenceReference {
1778                    kind: CapitalBookEvidenceKind::Receipt,
1779                    reference_id: "rcpt-web3-1".to_string(),
1780                    observed_at: Some(1_743_292_800),
1781                    locator: Some("receipt:rcpt-web3-1".to_string()),
1782                }],
1783                description: "release escrow over the official web3 rail".to_string(),
1784            },
1785            &signer,
1786        )
1787        .unwrap()
1788    }
1789
1790    fn sample_dispatch() -> Web3SettlementDispatchArtifact {
1791        Web3SettlementDispatchArtifact {
1792            schema: CHIO_WEB3_SETTLEMENT_DISPATCH_SCHEMA.to_string(),
1793            dispatch_id: "dispatch-web3-1".to_string(),
1794            issued_at: 1_743_292_800,
1795            trust_profile_id: "chio.official-web3-stack".to_string(),
1796            contract_package_id: "chio.official-web3-contracts".to_string(),
1797            chain_id: "eip155:8453".to_string(),
1798            capital_instruction: sample_capital_instruction(),
1799            bond: None,
1800            settlement_path: Web3SettlementPath::MerkleProof,
1801            settlement_amount: MonetaryAmount {
1802                units: 150,
1803                currency: "USD".to_string(),
1804            },
1805            escrow_id: "escrow-web3-1".to_string(),
1806            escrow_contract: "0x1000000000000000000000000000000000000002".to_string(),
1807            bond_vault_contract: "0x1000000000000000000000000000000000000003".to_string(),
1808            beneficiary_address: "0x2222222222222222222222222222222222222222".to_string(),
1809            support_boundary: Web3SettlementSupportBoundary {
1810                real_dispatch_supported: true,
1811                anchor_proof_required: true,
1812                oracle_evidence_required_for_fx: true,
1813                custody_boundary_explicit: true,
1814                reversal_supported: true,
1815            },
1816            note: Some(
1817                "Dispatches one governed escrow release over the official Base-first contract stack."
1818                    .to_string(),
1819            ),
1820        }
1821    }
1822
1823    fn sample_execution_receipt() -> Web3SettlementExecutionReceiptArtifact {
1824        Web3SettlementExecutionReceiptArtifact {
1825            schema: CHIO_WEB3_SETTLEMENT_RECEIPT_SCHEMA.to_string(),
1826            execution_receipt_id: "receipt-web3-1".to_string(),
1827            issued_at: 1_743_292_860,
1828            dispatch: sample_dispatch(),
1829            observed_execution: CapitalExecutionObservation {
1830                observed_at: 1_743_292_860,
1831                external_reference_id:
1832                    "0xcccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
1833                        .to_string(),
1834                amount: MonetaryAmount {
1835                    units: 150,
1836                    currency: "USD".to_string(),
1837                },
1838            },
1839            lifecycle_state: Web3SettlementLifecycleState::Settled,
1840            settlement_reference: "settlement-web3-1".to_string(),
1841            reconciled_anchor_proof: Some(sample_anchor_inclusion_proof()),
1842            oracle_evidence: Some(sample_oracle_evidence()),
1843            settled_amount: MonetaryAmount {
1844                units: 150,
1845                currency: "USD".to_string(),
1846            },
1847            reversal_of: None,
1848            failure_reason: None,
1849            note: Some(
1850                "Settled against an anchored receipt root and retained oracle provenance for the FX conversion."
1851                    .to_string(),
1852            ),
1853        }
1854    }
1855
1856    #[test]
1857    fn trust_profile_requires_local_policy_activation() {
1858        let mut profile = sample_trust_profile();
1859        profile.local_policy_activation_required = false;
1860        assert!(matches!(
1861            validate_web3_trust_profile(&profile),
1862            Err(Web3ContractError::InvalidBinding(_))
1863        ));
1864    }
1865
1866    #[test]
1867    fn identity_binding_signature_verifies() {
1868        verify_web3_identity_binding(&sample_binding()).unwrap();
1869    }
1870
1871    #[test]
1872    fn anchor_inclusion_proof_verifies_receipt_and_merkle_root() {
1873        verify_anchor_inclusion_proof(&sample_anchor_inclusion_proof()).unwrap();
1874    }
1875
1876    #[test]
1877    fn oracle_evidence_requires_non_zero_denominator() {
1878        let mut evidence = sample_oracle_evidence();
1879        evidence.rate_denominator = 0;
1880        assert!(matches!(
1881            validate_oracle_conversion_evidence(&evidence),
1882            Err(Web3ContractError::InvalidProof(_))
1883        ));
1884    }
1885
1886    #[test]
1887    fn oracle_evidence_rejects_unknown_authority() {
1888        let mut evidence = sample_oracle_evidence();
1889        evidence.authority = "unknown_authority".to_string();
1890        assert!(matches!(
1891            validate_oracle_conversion_evidence(&evidence),
1892            Err(Web3ContractError::InvalidProof(_))
1893        ));
1894    }
1895
1896    #[test]
1897    fn web3_dispatch_requires_web3_rail_kind() {
1898        let mut dispatch = sample_dispatch();
1899        dispatch.capital_instruction.body.rail.kind = CapitalExecutionRailKind::Api;
1900        assert!(matches!(
1901            validate_web3_settlement_dispatch(&dispatch),
1902            Err(Web3ContractError::InvalidSettlement(_))
1903        ));
1904    }
1905
1906    #[test]
1907    fn web3_dispatch_requires_completion_flow_binding_for_transfers() {
1908        let mut dispatch = sample_dispatch();
1909        dispatch.capital_instruction.body.completion_flow_row_id = None;
1910        assert!(matches!(
1911            validate_web3_settlement_dispatch(&dispatch),
1912            Err(Web3ContractError::MissingField(
1913                "web3_settlement_dispatch.capital_instruction.completion_flow_row_id"
1914            ))
1915        ));
1916    }
1917
1918    #[test]
1919    fn web3_dispatch_rejects_mismatched_completion_flow_binding() {
1920        let mut dispatch = sample_dispatch();
1921        dispatch.capital_instruction.body.completion_flow_row_id =
1922            Some("economic-completion-flow:other-receipt".to_string());
1923        assert!(matches!(
1924            validate_web3_settlement_dispatch(&dispatch),
1925            Err(Web3ContractError::InvalidSettlement(_))
1926        ));
1927    }
1928
1929    #[test]
1930    fn merkle_settlement_receipt_requires_anchor_proof() {
1931        let mut receipt = sample_execution_receipt();
1932        receipt.reconciled_anchor_proof = None;
1933        assert!(matches!(
1934            validate_web3_settlement_execution_receipt(&receipt),
1935            Err(Web3ContractError::InvalidSettlement(_))
1936        ));
1937    }
1938
1939    #[test]
1940    fn fx_sensitive_settlement_receipt_requires_oracle_evidence() {
1941        let mut receipt = sample_execution_receipt();
1942        receipt.oracle_evidence = None;
1943        assert!(matches!(
1944            validate_web3_settlement_execution_receipt(&receipt),
1945            Err(Web3ContractError::InvalidSettlement(_))
1946        ));
1947    }
1948
1949    #[test]
1950    fn reference_artifacts_parse_and_validate() {
1951        let trust_profile: Web3TrustProfile = serde_json::from_str(include_str!(
1952            "../../../docs/standards/CHIO_WEB3_TRUST_PROFILE.json"
1953        ))
1954        .unwrap();
1955        let contract_package: Web3ContractPackage = serde_json::from_str(include_str!(
1956            "../../../docs/standards/CHIO_WEB3_CONTRACT_PACKAGE.json"
1957        ))
1958        .unwrap();
1959        let chain_configuration: Web3ChainConfiguration = serde_json::from_str(include_str!(
1960            "../../../docs/standards/CHIO_WEB3_CHAIN_CONFIGURATION.json"
1961        ))
1962        .unwrap();
1963        let anchor_proof: AnchorInclusionProof = serde_json::from_str(include_str!(
1964            "../../../docs/standards/CHIO_ANCHOR_INCLUSION_PROOF_EXAMPLE.json"
1965        ))
1966        .unwrap();
1967        let dispatch: Web3SettlementDispatchArtifact = serde_json::from_str(include_str!(
1968            "../../../docs/standards/CHIO_WEB3_SETTLEMENT_DISPATCH_EXAMPLE.json"
1969        ))
1970        .unwrap();
1971        let receipt: Web3SettlementExecutionReceiptArtifact = serde_json::from_str(include_str!(
1972            "../../../docs/standards/CHIO_WEB3_SETTLEMENT_RECEIPT_EXAMPLE.json"
1973        ))
1974        .unwrap();
1975        let matrix: Web3QualificationMatrix = serde_json::from_str(include_str!(
1976            "../../../docs/standards/CHIO_WEB3_QUALIFICATION_MATRIX.json"
1977        ))
1978        .unwrap();
1979
1980        validate_web3_trust_profile(&trust_profile).unwrap();
1981        verify_web3_identity_binding(&trust_profile.operator_binding).unwrap();
1982        validate_web3_contract_package(&contract_package).unwrap();
1983        validate_web3_chain_configuration(&chain_configuration).unwrap();
1984        validate_anchor_inclusion_proof(&anchor_proof).unwrap();
1985        verify_anchor_inclusion_proof(&anchor_proof).unwrap();
1986        validate_web3_settlement_dispatch(&dispatch).unwrap();
1987        validate_web3_settlement_execution_receipt(&receipt).unwrap();
1988        validate_web3_qualification_matrix(&matrix).unwrap();
1989    }
1990}