Skip to main content

tai_core/
ids.rs

1//! Typed wrappers for Sui's 32-byte addresses and object IDs.
2//!
3//! Sui addresses and object IDs are both 32-byte values rendered as
4//! hex-prefixed-with-`0x`. We use distinct types so the API can express
5//! whether a parameter refers to a wallet address or a specific on-chain
6//! object — the bytes are interchangeable but the meaning is not.
7
8use crate::error::TaiError;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11use std::str::FromStr;
12
13/// Length of a Sui address / object ID in bytes.
14pub const SUI_ADDR_LEN: usize = 32;
15
16/// A 32-byte Sui address.
17#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(transparent)]
19pub struct SuiAddress(#[serde(with = "hex_prefixed")] pub [u8; SUI_ADDR_LEN]);
20
21/// A 32-byte Sui object ID.
22#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23#[serde(transparent)]
24pub struct ObjectId(#[serde(with = "hex_prefixed")] pub [u8; SUI_ADDR_LEN]);
25
26impl fmt::Debug for SuiAddress {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        write!(f, "0x{}", hex::encode(self.0))
29    }
30}
31
32impl fmt::Display for SuiAddress {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        write!(f, "0x{}", hex::encode(self.0))
35    }
36}
37
38impl fmt::Debug for ObjectId {
39    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
40        write!(f, "0x{}", hex::encode(self.0))
41    }
42}
43
44impl fmt::Display for ObjectId {
45    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46        write!(f, "0x{}", hex::encode(self.0))
47    }
48}
49
50impl FromStr for SuiAddress {
51    type Err = TaiError;
52    fn from_str(s: &str) -> Result<Self, Self::Err> {
53        Ok(SuiAddress(parse_32_hex(s)?))
54    }
55}
56
57impl FromStr for ObjectId {
58    type Err = TaiError;
59    fn from_str(s: &str) -> Result<Self, Self::Err> {
60        Ok(ObjectId(parse_32_hex(s)?))
61    }
62}
63
64impl SuiAddress {
65    /// The zero address (`0x000…000`). Commonly used as the sender for
66    /// `dev_inspect_transaction_block` calls that only need read access.
67    pub const ZERO: SuiAddress = SuiAddress([0u8; SUI_ADDR_LEN]);
68
69    /// Construct from a 32-byte array.
70    pub fn from_bytes(b: [u8; SUI_ADDR_LEN]) -> Self {
71        SuiAddress(b)
72    }
73
74    /// Borrow the underlying bytes.
75    pub fn as_bytes(&self) -> &[u8; SUI_ADDR_LEN] {
76        &self.0
77    }
78}
79
80impl ObjectId {
81    /// Construct from a 32-byte array.
82    pub fn from_bytes(b: [u8; SUI_ADDR_LEN]) -> Self {
83        ObjectId(b)
84    }
85
86    /// Borrow the underlying bytes.
87    pub fn as_bytes(&self) -> &[u8; SUI_ADDR_LEN] {
88        &self.0
89    }
90}
91
92fn parse_32_hex(s: &str) -> Result<[u8; SUI_ADDR_LEN], TaiError> {
93    let s = s.strip_prefix("0x").unwrap_or(s);
94    // Sui allows leading-zero compression in CLI output (e.g. "0x6"); for
95    // robustness, left-pad to 64 hex chars.
96    let padded: String = if s.len() < 64 {
97        format!("{:0>64}", s)
98    } else {
99        s.to_string()
100    };
101    if padded.len() != 64 {
102        return Err(TaiError::InvalidAddress(format!(
103            "expected 32 bytes (64 hex chars), got {}",
104            s.len()
105        )));
106    }
107    let bytes =
108        hex::decode(&padded).map_err(|e| TaiError::InvalidAddress(format!("hex decode: {e}")))?;
109    let mut out = [0u8; SUI_ADDR_LEN];
110    out.copy_from_slice(&bytes);
111    Ok(out)
112}
113
114/// Serde helper for `[u8; 32]` <-> hex-prefixed string.
115mod hex_prefixed {
116    use serde::{Deserialize, Deserializer, Serializer};
117
118    pub fn serialize<S: Serializer>(b: &[u8; 32], s: S) -> Result<S::Ok, S::Error> {
119        s.serialize_str(&format!("0x{}", hex::encode(b)))
120    }
121
122    pub fn deserialize<'de, D: Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> {
123        let s = String::deserialize(d)?;
124        let s = s.strip_prefix("0x").unwrap_or(&s);
125        let padded: String = if s.len() < 64 {
126            format!("{:0>64}", s)
127        } else {
128            s.to_string()
129        };
130        let bytes = hex::decode(&padded).map_err(serde::de::Error::custom)?;
131        if bytes.len() != 32 {
132            return Err(serde::de::Error::custom("expected 32 bytes"));
133        }
134        let mut out = [0u8; 32];
135        out.copy_from_slice(&bytes);
136        Ok(out)
137    }
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143
144    #[test]
145    fn parse_full_64_hex() {
146        let s = "0x7d41072ae77b18b752292b47468e07e6332cd9a6ef9b052752f98f22d9844f8d";
147        let id: ObjectId = s.parse().unwrap();
148        assert_eq!(id.to_string(), s);
149    }
150
151    #[test]
152    fn parse_short_hex_is_left_padded() {
153        let id: ObjectId = "0x6".parse().unwrap();
154        assert_eq!(
155            id.to_string(),
156            "0x0000000000000000000000000000000000000000000000000000000000000006"
157        );
158    }
159
160    #[test]
161    fn parse_without_prefix() {
162        let id: SuiAddress = "ad".parse().unwrap();
163        assert_eq!(
164            id.to_string(),
165            "0x00000000000000000000000000000000000000000000000000000000000000ad"
166        );
167    }
168
169    #[test]
170    fn rejects_too_long() {
171        let s = "0x".to_string() + &"a".repeat(65);
172        assert!(s.parse::<ObjectId>().is_err());
173    }
174
175    #[test]
176    fn zero_address_constant() {
177        assert_eq!(
178            SuiAddress::ZERO.to_string(),
179            "0x0000000000000000000000000000000000000000000000000000000000000000"
180        );
181    }
182
183    #[test]
184    fn json_roundtrip() {
185        let id: ObjectId = "0x7d41072ae77b18b752292b47468e07e6332cd9a6ef9b052752f98f22d9844f8d"
186            .parse()
187            .unwrap();
188        let json = serde_json::to_string(&id).unwrap();
189        assert_eq!(
190            json,
191            "\"0x7d41072ae77b18b752292b47468e07e6332cd9a6ef9b052752f98f22d9844f8d\""
192        );
193        let id2: ObjectId = serde_json::from_str(&json).unwrap();
194        assert_eq!(id, id2);
195    }
196}