nexus-sdk 2.1.1

Rust SDK for Nexus protocol types, RPC access, and transaction construction.
Documentation
//! JSON parsing and transaction space estimates for task input plans.

use {crate::types::NexusData, serde_json::Value, std::collections::HashSet};

const NEXUS_BASE_TRANSACTION_SIZE: usize = 8 * 1024;
const MAX_TRANSACTION_SIZE: usize = 128 * 1024;
const ENTRY_PORTS_RESERVED_BYTES: usize = 64 * 1024;
const WALRUS_BLOB_ID_LENGTH: usize = 44;
const SHA256_DIGEST_LENGTH: usize = 32;

pub(crate) fn nexus_data_from_json_value(data: Value) -> anyhow::Result<NexusData> {
    if is_canonical_nexus_data(&data) {
        return NexusData::from_json_value(&data);
    }

    match data {
        Value::Array(values) => NexusData::inline_data_many(
            values
                .into_iter()
                .map(|value| serde_json::to_vec(&value))
                .collect::<Result<Vec<_>, _>>()?,
        ),
        value => NexusData::inline_data(serde_json::to_vec(&value)?),
    }
}

pub(crate) fn is_canonical_nexus_data(value: &Value) -> bool {
    let Some(object) = value.as_object().filter(|object| object.len() == 1) else {
        return false;
    };

    match (object.get("one"), object.get("many")) {
        (Some(value), None) => value.get("kind").is_some(),
        (None, Some(Value::Array(values))) => {
            values.iter().all(|value| value.get("kind").is_some())
        }
        _ => false,
    }
}

#[cfg(test)]
pub(crate) fn nexus_data_to_json_value(data: &NexusData) -> Value {
    use crate::move_bindings::primitives::data::NexusValue;

    let decode = |value: &NexusValue| match value {
        NexusValue::InlineData { bytes } => decode_nexus_data_json(bytes),
        NexusValue::Object { .. } | NexusValue::WalrusData { .. } => Value::Null,
    };
    let values = data.values().unwrap_or_default();
    if data.is_one() {
        values.first().map(decode).unwrap_or(Value::Null)
    } else {
        Value::Array(values.iter().map(decode).collect())
    }
}

pub(crate) fn hint_remote_fields(
    json: &Value,
    selected_remote_fields: &HashSet<String>,
) -> anyhow::Result<Vec<String>> {
    let Some(obj) = json.as_object() else {
        anyhow::bail!("Expected JSON object");
    };

    let mut fields: Vec<(&String, &Value, usize)> = obj
        .iter()
        .map(|(key, value)| (key, value, key.len() + value.to_string().len()))
        .collect();

    fields.sort_by_key(|(_, _, inline_size)| std::cmp::Reverse(*inline_size));

    let available_size = (MAX_TRANSACTION_SIZE - NEXUS_BASE_TRANSACTION_SIZE)
        .saturating_sub(ENTRY_PORTS_RESERVED_BYTES);
    let mut required_size = fields
        .iter()
        .fold(0usize, |total, (key, value, inline_size)| {
            total.saturating_add(if selected_remote_fields.contains(*key) {
                walrus_reference_value_size(value)
            } else {
                *inline_size
            })
        });

    if required_size <= available_size {
        return Ok(vec![]);
    }

    let mut remote_fields = vec![];
    for (key, value, inline_size) in fields {
        if selected_remote_fields.contains(key) {
            continue;
        }
        let key = key.clone();
        let storage_cost = walrus_reference_value_size(value);

        required_size = required_size
            .saturating_sub(inline_size)
            .saturating_add(storage_cost);
        remote_fields.push(key);

        if required_size <= available_size {
            break;
        }
    }

    if required_size > available_size {
        anyhow::bail!(
            "Cannot fit data within max transaction size, even after storing all fields remotely"
        );
    }

    Ok(remote_fields)
}

fn walrus_reference_value_size(value: &Value) -> usize {
    match value {
        Value::Array(values) => walrus_reference_input_size().saturating_mul(values.len()),
        _ => walrus_reference_input_size(),
    }
}

fn walrus_reference_input_size() -> usize {
    encoded_pure_vector_size(WALRUS_BLOB_ID_LENGTH) + encoded_pure_vector_size(SHA256_DIGEST_LENGTH)
}

fn encoded_pure_vector_size(value_len: usize) -> usize {
    let move_value_len = uleb128_size(value_len) + value_len;
    1 + uleb128_size(move_value_len) + move_value_len
}

fn uleb128_size(mut value: usize) -> usize {
    let mut size = 1;
    while value >= 128 {
        value >>= 7;
        size += 1;
    }
    size
}

#[cfg(test)]
fn decode_nexus_data_json(bytes: &[u8]) -> Value {
    let text = std::str::from_utf8(bytes).expect("NexusData JSON bytes must be UTF-8");
    let adjusted = wrap_large_numbers_as_string(text.trim());
    serde_json::from_str(&adjusted).unwrap_or_else(|_| Value::String(text.to_string()))
}

#[cfg(test)]
fn is_large_number(s: &str) -> bool {
    if let Some(stripped) = s.strip_prefix('-') {
        stripped.chars().all(|c| c.is_ascii_digit()) && s.len() > 21
    } else {
        s.chars().all(|c| c.is_ascii_digit()) && s.len() > 20
    }
}

#[cfg(test)]
fn wrap_large_numbers_as_string(value: &str) -> String {
    if is_large_number(value) {
        format!(r#""{value}""#)
    } else {
        value.to_string()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn maximal_many_fields(count: usize) -> Value {
        Value::Object(
            (0..count)
                .map(|index| {
                    (
                        format!("field-{index}"),
                        Value::Array((0..128).map(|_| Value::String("x".repeat(100))).collect()),
                    )
                })
                .collect(),
        )
    }

    #[test]
    fn plain_json_roundtrips_without_inline_base64_shape() {
        let original = serde_json::json!({
            "object": { "nested": true },
            "number": 7,
        });
        let data = nexus_data_from_json_value(original.clone()).unwrap();

        assert_eq!(nexus_data_to_json_value(&data), original);
    }

    #[test]
    fn ordinary_one_property_object_remains_inline_data() {
        let original = serde_json::json!({ "one": { "nested": true } });
        let data = nexus_data_from_json_value(original.clone()).unwrap();

        assert!(data.is_data());
        assert_eq!(nexus_data_to_json_value(&data), original);
    }

    #[test]
    fn canonical_one_object_preserves_object_kind() {
        let data = nexus_data_from_json_value(serde_json::json!({
            "one": { "kind": "object", "id": "0x42" }
        }))
        .unwrap();

        assert!(data.is_one());
        assert!(data.values().unwrap()[0].is_object());
    }

    #[test]
    fn canonical_many_objects_preserve_object_kind() {
        let data = nexus_data_from_json_value(serde_json::json!({
            "many": [
                { "kind": "object", "id": "0x42" },
                { "kind": "object", "id": "0x43" }
            ]
        }))
        .unwrap();

        assert!(data.is_many());
        assert!(data.values().unwrap().iter().all(|value| value.is_object()));
    }

    #[test]
    fn malformed_canonical_object_is_rejected() {
        let error = nexus_data_from_json_value(serde_json::json!({
            "one": { "kind": "object", "id": "not-an-object" }
        }))
        .unwrap_err();

        assert!(error.to_string().contains("invalid Object ID"));
    }

    #[test]
    fn array_input_remains_ordered_independent_many_values() {
        let original = serde_json::json!([1, { "ordered": 2 }, "three"]);
        let data = nexus_data_from_json_value(original.clone()).unwrap();

        assert!(data.is_many());
        assert_eq!(nexus_data_to_json_value(&data), original);
    }

    #[test]
    fn empty_array_input_is_rejected() {
        let error = nexus_data_from_json_value(serde_json::json!([])).unwrap_err();

        assert!(error.to_string().contains("requires at least one value"));
    }

    #[test]
    fn walrus_reference_size_includes_digest_and_bcs_encoding() {
        assert_eq!(walrus_reference_input_size(), 82);
    }

    #[test]
    fn maximal_many_walrus_references_respect_near_limit_budget() {
        let selected_remote_fields = HashSet::new();
        assert_eq!(
            hint_remote_fields(&maximal_many_fields(5), &selected_remote_fields)
                .unwrap()
                .len(),
            4
        );
        assert!(hint_remote_fields(&maximal_many_fields(6), &selected_remote_fields).is_err());
    }
}