exochain-wasm 0.2.0-beta

ExoChain governance engine — WebAssembly bindings for Node.js
Documentation
// Copyright 2026 Exochain Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at:
//
//     https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// SPDX-License-Identifier: Apache-2.0

//! Economy bindings: HonorGood object validation and deterministic anchors.

use exo_core::Hash256;
use exo_economy::{
    EconomyObjectKind, EconomyRecordAnchor, HonorGoodRuleset, LegacyReceipt, Mission,
    ValueContributionNode,
};
use serde::Serialize;
use wasm_bindgen::prelude::*;

use crate::serde_bridge::{from_json_str, to_js_value};

const HASH256_HEX_LEN: usize = 64;
const EXOCHAIN_SETTLEMENT_AUTHORITY: &str = "EXOCHAIN";

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub(crate) struct WasmEconomyAnchor<T> {
    pub settlement_authority: &'static str,
    pub local_settlement_authority: bool,
    pub object: T,
    pub anchor: EconomyRecordAnchor,
}

fn js_error(message: &str) -> JsValue {
    JsValue::from_str(message)
}

fn parse_anchor_hash_hex(value: &str) -> Result<Hash256, JsValue> {
    let trimmed = value.trim();
    if trimmed.is_empty() {
        return Ok(Hash256::ZERO);
    }
    if trimmed.len() != HASH256_HEX_LEN {
        return Err(js_error("previous anchor hash must be 64 hex characters"));
    }
    let bytes = hex::decode(trimmed).map_err(|_| js_error("previous anchor hash must be hex"))?;
    let array: [u8; 32] = bytes
        .try_into()
        .map_err(|_| js_error("previous anchor hash must decode to 32 bytes"))?;
    Ok(Hash256::from_bytes(array))
}

fn economy_anchor<T: Serialize>(
    object: T,
    object_kind: EconomyObjectKind,
    object_id: Hash256,
    object_hash: Hash256,
    created_at: exo_core::Timestamp,
    previous_anchor_hash: Hash256,
) -> Result<WasmEconomyAnchor<T>, exo_economy::EconomyError> {
    let anchor = EconomyRecordAnchor {
        anchor_hash: Hash256::ZERO,
        previous_anchor_hash,
        object_kind,
        object_id,
        object_hash,
        created_at,
    }
    .anchor()?;

    Ok(WasmEconomyAnchor {
        settlement_authority: EXOCHAIN_SETTLEMENT_AUTHORITY,
        local_settlement_authority: false,
        object,
        anchor,
    })
}

pub(crate) fn mission_anchor(
    mission: Mission,
    previous_anchor_hash: Hash256,
) -> Result<WasmEconomyAnchor<Mission>, exo_economy::EconomyError> {
    let anchored = mission.anchor()?;
    economy_anchor(
        anchored.clone(),
        EconomyObjectKind::Mission,
        anchored.mission_id,
        anchored.content_hash,
        anchored.created_at,
        previous_anchor_hash,
    )
}

pub(crate) fn legacy_receipt_anchor(
    receipt: LegacyReceipt,
    previous_anchor_hash: Hash256,
) -> Result<WasmEconomyAnchor<LegacyReceipt>, exo_economy::EconomyError> {
    let anchored = receipt.anchor()?;
    economy_anchor(
        anchored.clone(),
        EconomyObjectKind::LegacyReceipt,
        anchored.legacy_receipt_id,
        anchored.content_hash,
        anchored.created_at,
        previous_anchor_hash,
    )
}

pub(crate) fn ruleset_anchor(
    ruleset: HonorGoodRuleset,
    previous_anchor_hash: Hash256,
) -> Result<WasmEconomyAnchor<HonorGoodRuleset>, exo_economy::EconomyError> {
    let anchored = ruleset.anchor()?;
    economy_anchor(
        anchored.clone(),
        EconomyObjectKind::HonorGoodRuleset,
        anchored.ruleset_id,
        anchored.content_hash,
        anchored.created_at,
        previous_anchor_hash,
    )
}

pub(crate) fn value_contribution_node_anchor(
    node: ValueContributionNode,
    previous_anchor_hash: Hash256,
) -> Result<WasmEconomyAnchor<ValueContributionNode>, exo_economy::EconomyError> {
    let anchored = node.anchor()?;
    economy_anchor(
        anchored.clone(),
        EconomyObjectKind::ValueContributionNode,
        anchored.contribution_node_id,
        anchored.content_hash,
        anchored.created_at_hlc,
        previous_anchor_hash,
    )
}

#[wasm_bindgen]
pub fn wasm_anchor_economy_mission(
    mission_json: &str,
    previous_anchor_hash_hex: &str,
) -> Result<JsValue, JsValue> {
    let mission: Mission = from_json_str(mission_json)?;
    let previous = parse_anchor_hash_hex(previous_anchor_hash_hex)?;
    let report = mission_anchor(mission, previous)
        .map_err(|err| js_error(&format!("economy mission rejected: {err}")))?;
    to_js_value(&report)
}

#[wasm_bindgen]
pub fn wasm_anchor_economy_legacy_receipt(
    legacy_receipt_json: &str,
    previous_anchor_hash_hex: &str,
) -> Result<JsValue, JsValue> {
    let receipt: LegacyReceipt = from_json_str(legacy_receipt_json)?;
    let previous = parse_anchor_hash_hex(previous_anchor_hash_hex)?;
    let report = legacy_receipt_anchor(receipt, previous)
        .map_err(|err| js_error(&format!("economy legacy receipt rejected: {err}")))?;
    to_js_value(&report)
}

#[wasm_bindgen]
pub fn wasm_anchor_economy_ruleset(
    ruleset_json: &str,
    previous_anchor_hash_hex: &str,
) -> Result<JsValue, JsValue> {
    let ruleset: HonorGoodRuleset = from_json_str(ruleset_json)?;
    let previous = parse_anchor_hash_hex(previous_anchor_hash_hex)?;
    let report = ruleset_anchor(ruleset, previous)
        .map_err(|err| js_error(&format!("economy ruleset rejected: {err}")))?;
    to_js_value(&report)
}

#[wasm_bindgen]
pub fn wasm_anchor_economy_value_contribution_node(
    node_json: &str,
    previous_anchor_hash_hex: &str,
) -> Result<JsValue, JsValue> {
    let node: ValueContributionNode = from_json_str(node_json)?;
    let previous = parse_anchor_hash_hex(previous_anchor_hash_hex)?;
    let report = value_contribution_node_anchor(node, previous)
        .map_err(|err| js_error(&format!("economy value contribution node rejected: {err}")))?;
    to_js_value(&report)
}

#[cfg(test)]
mod tests {
    use exo_core::{Did, Timestamp};
    use exo_economy::{
        MissionPurpose, MissionStatus, MissionType, ParticipantRef, ValueContributionStatus,
    };

    use super::*;

    fn h(byte: u8) -> Hash256 {
        Hash256::from_bytes([byte; 32])
    }

    fn did(label: &str) -> Did {
        Did::new(&format!("did:exo:{label}")).unwrap()
    }

    fn sample_mission() -> Mission {
        Mission {
            mission_id: Hash256::ZERO,
            name: "HonorGood WASM mission".into(),
            mission_type: MissionType::UpstreamRecognition,
            owner_did: did("owner"),
            principal_did: did("principal"),
            purpose: MissionPurpose {
                problem: "adapter validation".into(),
                served_party: "EXOCHAIN".into(),
                promised_outcome: "deterministic anchor".into(),
                expected_value: "stable hash".into(),
                risk_surface: "WASM boundary".into(),
                proof_required: "canonical CBOR hash".into(),
                success_condition: "same input same anchor".into(),
            },
            related_platforms: vec!["EXOCHAIN".into()],
            expected_value_micro_exo: None,
            ruleset_id: h(0x11),
            status: MissionStatus::Active,
            created_at: Timestamp::new(42_000, 0),
            content_hash: Hash256::ZERO,
        }
    }

    fn sample_node() -> ValueContributionNode {
        ValueContributionNode {
            contribution_node_id: Hash256::ZERO,
            contributor_ref: ParticipantRef::ProjectTreasury {
                project: "Archon".into(),
                treasury_ref: "public-project-treasury:Archon".into(),
            },
            contributor_type: exo_economy::ContributorType::Project,
            contribution_name: "Archon".into(),
            contribution_type: exo_economy::ContributionType::Code,
            source_uri: Some("https://github.com/coleam00/Archon".into()),
            evidence_hash: h(0x21),
            provenance_hash: h(0x22),
            license_or_compact_ref: "MIT".into(),
            honor_good_terms_hash: h(0x23),
            bailment_terms_hash: h(0x24),
            settlement_ruleset_id: h(0x25),
            beneficiary_ref: ParticipantRef::ProjectTreasury {
                project: "Archon".into(),
                treasury_ref: "public-project-treasury:Archon".into(),
            },
            materiality_policy_id: h(0x26),
            adoption_policy_id: h(0x27),
            revocation_policy_id: h(0x28),
            dispute_policy_id: h(0x29),
            status: ValueContributionStatus::Active,
            created_at_hlc: Timestamp::new(43_000, 0),
            content_hash: Hash256::ZERO,
        }
    }

    #[test]
    fn mission_anchor_is_deterministic_and_non_authoritative_locally() {
        let first = mission_anchor(sample_mission(), Hash256::ZERO).unwrap();
        let second = mission_anchor(sample_mission(), Hash256::ZERO).unwrap();

        assert_eq!(first.object.mission_id, second.object.mission_id);
        assert_eq!(first.anchor.anchor_hash, second.anchor.anchor_hash);
        assert_eq!(first.anchor.object_kind, EconomyObjectKind::Mission);
        assert_eq!(first.settlement_authority, "EXOCHAIN");
        assert!(!first.local_settlement_authority);
    }

    #[test]
    fn value_contribution_node_anchor_rejects_invalid_input() {
        let mut node = sample_node();
        node.evidence_hash = Hash256::ZERO;

        assert!(value_contribution_node_anchor(node, Hash256::ZERO).is_err());
    }
}