Skip to main content

accumulate_client/codec/
mod.rs

1//! Codec utilities for Accumulate protocol compatibility
2//!
3//! This module provides both JSON and binary encoding to match the TypeScript SDK
4//! implementation for bit-for-bit and byte-for-byte parity.
5
6#![allow(missing_docs)]
7
8use serde_json::{Map, Value};
9use sha2::{Digest, Sha256};
10use std::collections::BTreeMap;
11
12pub mod canonical;
13pub mod crypto;
14pub mod hash_helper;
15pub mod hashes;
16pub mod reader;
17pub mod signing;
18pub mod transaction_codec;
19pub mod writer;
20
21pub use canonical::*;
22// crypto module is kept for backwards compatibility but has no exports
23// pub use crypto::*;
24pub use hash_helper::*;
25pub use hashes::*;
26pub use reader::*;
27pub use signing::*;
28pub use transaction_codec::*;
29pub use writer::*;
30
31/// Convert a JSON value to canonical JSON string with deterministic ordering
32/// This matches the TypeScript SDK implementation exactly
33pub fn canonical_json(value: &Value) -> String {
34    crate::canonjson::canonicalize(value)
35}
36
37/// Convert any serializable value to canonical JSON
38/// Convenience wrapper around canonjson::dumps_canonical
39pub fn to_canonical_string<T: serde::Serialize>(value: &T) -> String {
40    crate::canonjson::dumps_canonical(value)
41}
42
43/// SHA-256 hash of raw bytes
44pub fn sha256_bytes(data: &[u8]) -> [u8; 32] {
45    let mut hasher = Sha256::new();
46    hasher.update(data);
47    hasher.finalize().into()
48}
49
50/// SHA-256 hash of a JSON value via canonical JSON
51pub fn sha256_hex(value: &Value) -> String {
52    let canonical = canonical_json(value);
53    let hash = sha256_bytes(canonical.as_bytes());
54    hex::encode(hash)
55}
56
57/// Deterministic JSON object conversion ensuring sorted keys
58pub fn canonicalize_value(value: &Value) -> Value {
59    match value {
60        Value::Object(map) => {
61            let mut btree: BTreeMap<String, Value> = BTreeMap::new();
62            for (k, v) in map {
63                btree.insert(k.clone(), canonicalize_value(v));
64            }
65            Value::Object(Map::from_iter(btree.into_iter()))
66        }
67        Value::Array(arr) => Value::Array(arr.iter().map(canonicalize_value).collect()),
68        _ => value.clone(),
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use serde_json::json;
76
77    #[test]
78    fn test_canonical_json_simple() {
79        let value = json!({ "z": 3, "a": 1, "m": 2 });
80        let canonical = canonical_json(&value);
81        assert_eq!(canonical, r#"{"a":1,"m":2,"z":3}"#);
82    }
83
84    #[test]
85    fn test_canonical_json_nested() {
86        let value = json!({
87            "z": { "y": 2, "x": 1 },
88            "a": 1
89        });
90        let canonical = canonical_json(&value);
91        assert_eq!(canonical, r#"{"a":1,"z":{"x":1,"y":2}}"#);
92    }
93
94    #[test]
95    fn test_canonical_json_array() {
96        let value = json!({
97            "arr": [{ "b": 2, "a": 1 }, { "d": 4, "c": 3 }]
98        });
99        let canonical = canonical_json(&value);
100        assert_eq!(canonical, r#"{"arr":[{"a":1,"b":2},{"c":3,"d":4}]}"#);
101    }
102
103    #[test]
104    fn test_canonical_json_primitives() {
105        let value = json!({
106            "string": "test",
107            "number": 42,
108            "boolean": true,
109            "null": null
110        });
111        let canonical = canonical_json(&value);
112        assert_eq!(
113            canonical,
114            r#"{"boolean":true,"null":null,"number":42,"string":"test"}"#
115        );
116    }
117
118    #[test]
119    fn test_sha256_consistency() {
120        let value = json!({
121            "header": {
122                "principal": "acc://alice.acme/tokens",
123                "timestamp": 1234567890123u64
124            },
125            "body": {
126                "type": "send-tokens",
127                "to": [{
128                    "url": "acc://bob.acme/tokens",
129                    "amount": "1000"
130                }]
131            }
132        });
133
134        let canonical = canonical_json(&value);
135        let hash = sha256_hex(&value);
136
137        // This should match the expected hash from our TS fixture
138        let expected_canonical = r#"{"body":{"to":[{"amount":"1000","url":"acc://bob.acme/tokens"}],"type":"send-tokens"},"header":{"principal":"acc://alice.acme/tokens","timestamp":1234567890123}}"#;
139        let expected_hash = "4be49c59c717f1984646998cecac0e5225378d9bbe2e18928272a85b7dfcb608";
140
141        assert_eq!(canonical, expected_canonical);
142        assert_eq!(hash, expected_hash);
143    }
144}