iota_graphql_rpc/types/
iota_address.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::str::FromStr;
6
7use async_graphql::*;
8use iota_types::base_types::{IotaAddress as NativeIotaAddress, ObjectID};
9use move_core_types::account_address::AccountAddress;
10use serde::{Deserialize, Serialize};
11
12use crate::error::Error;
13
14const IOTA_ADDRESS_LENGTH: usize = 32;
15
16#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash, Copy)]
17pub(crate) struct IotaAddress([u8; IOTA_ADDRESS_LENGTH]);
18
19#[derive(thiserror::Error, Debug, Eq, PartialEq)]
20pub(crate) enum FromStrError {
21    #[error("Invalid IotaAddress. Missing 0x prefix.")]
22    NoPrefix,
23
24    #[error(
25        "Expected IotaAddress string with between 1 and {} digits ({} bytes), received {0}",
26        IOTA_ADDRESS_LENGTH * 2,
27        IOTA_ADDRESS_LENGTH,
28    )]
29    WrongLength(usize),
30
31    #[error("Invalid character {0:?} at position {1}")]
32    BadHex(char, usize),
33}
34
35#[derive(thiserror::Error, Debug, Eq, PartialEq)]
36pub(crate) enum FromVecError {
37    #[error(
38        "Expected IotaAddress with {} bytes, received {0}",
39        IOTA_ADDRESS_LENGTH
40    )]
41    WrongLength(usize),
42}
43
44impl IotaAddress {
45    pub fn from_array(arr: [u8; IOTA_ADDRESS_LENGTH]) -> Self {
46        IotaAddress(arr)
47    }
48
49    pub fn into_vec(self) -> Vec<u8> {
50        self.0.to_vec()
51    }
52
53    pub fn as_slice(&self) -> &[u8] {
54        &self.0
55    }
56
57    pub fn from_bytes<T: AsRef<[u8]>>(bytes: T) -> Result<Self, FromVecError> {
58        <[u8; IOTA_ADDRESS_LENGTH]>::try_from(bytes.as_ref())
59            .map_err(|_| FromVecError::WrongLength(bytes.as_ref().len()))
60            .map(IotaAddress)
61    }
62}
63
64#[Scalar(use_type_description = true)]
65impl ScalarType for IotaAddress {
66    fn parse(value: Value) -> InputValueResult<Self> {
67        let Value::String(s) = value else {
68            return Err(InputValueError::expected_type(value));
69        };
70
71        Ok(IotaAddress::from_str(&s)?)
72    }
73
74    fn to_value(&self) -> Value {
75        Value::String(format!("0x{}", hex::encode(self.0)))
76    }
77}
78
79impl Description for IotaAddress {
80    fn description() -> &'static str {
81        "String containing 32B hex-encoded address, with a leading \"0x\". Leading zeroes can be \
82         omitted on input but will always appear in outputs (IotaAddress in output is guaranteed \
83         to be 66 characters long)."
84    }
85}
86
87impl TryFrom<Vec<u8>> for IotaAddress {
88    type Error = FromVecError;
89
90    fn try_from(bytes: Vec<u8>) -> Result<Self, FromVecError> {
91        Self::from_bytes(bytes)
92    }
93}
94
95impl From<AccountAddress> for IotaAddress {
96    fn from(value: AccountAddress) -> Self {
97        IotaAddress(value.into_bytes())
98    }
99}
100
101impl From<IotaAddress> for AccountAddress {
102    fn from(value: IotaAddress) -> Self {
103        AccountAddress::new(value.0)
104    }
105}
106
107impl From<ObjectID> for IotaAddress {
108    fn from(value: ObjectID) -> Self {
109        IotaAddress(value.into_bytes())
110    }
111}
112
113impl From<IotaAddress> for ObjectID {
114    fn from(value: IotaAddress) -> Self {
115        ObjectID::new(value.0)
116    }
117}
118
119impl From<NativeIotaAddress> for IotaAddress {
120    fn from(value: NativeIotaAddress) -> Self {
121        IotaAddress(value.to_inner())
122    }
123}
124
125impl From<IotaAddress> for NativeIotaAddress {
126    fn from(value: IotaAddress) -> Self {
127        AccountAddress::from(value).into()
128    }
129}
130
131impl FromStr for IotaAddress {
132    type Err = FromStrError;
133
134    fn from_str(s: &str) -> Result<Self, FromStrError> {
135        let Some(s) = s.strip_prefix("0x") else {
136            return Err(FromStrError::NoPrefix);
137        };
138
139        if s.is_empty() || s.len() > IOTA_ADDRESS_LENGTH * 2 {
140            return Err(FromStrError::WrongLength(s.len()));
141        }
142
143        let mut arr = [0u8; IOTA_ADDRESS_LENGTH];
144        hex::decode_to_slice(
145            // Left pad with `0`-s up to IOTA_ADDRESS_LENGTH * 2 characters long.
146            format!("{:0>width$}", s, width = IOTA_ADDRESS_LENGTH * 2),
147            &mut arr[..],
148        )
149        .map_err(|e| match e {
150            hex::FromHexError::InvalidHexCharacter { c, index } => {
151                FromStrError::BadHex(c, index + 2)
152            }
153            hex::FromHexError::OddLength => unreachable!("SAFETY: Prevented by padding"),
154            hex::FromHexError::InvalidStringLength => {
155                unreachable!("SAFETY: Prevented by bounds check")
156            }
157        })?;
158
159        Ok(IotaAddress(arr))
160    }
161}
162
163impl std::fmt::Display for IotaAddress {
164    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
165        f.write_str(&format!("0x{}", hex::encode(self.0)))
166    }
167}
168
169/// Parse a `IotaAddress` from its stored representation.  Failure is an
170/// internal error: the database should never contain a malformed address
171/// (containing the wrong number of bytes).
172pub(crate) fn addr(bytes: impl AsRef<[u8]>) -> Result<IotaAddress, Error> {
173    IotaAddress::from_bytes(bytes.as_ref()).map_err(|e| {
174        let bytes = bytes.as_ref().to_vec();
175        Error::Internal(format!("Error deserializing address: {bytes:?}: {e}"))
176    })
177}
178
179#[cfg(test)]
180mod tests {
181    use async_graphql::Value;
182
183    use super::*;
184
185    const STR_ADDRESS: &str = "0x0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
186    const ARR_ADDRESS: [u8; IOTA_ADDRESS_LENGTH] = [
187        1, 35, 69, 103, 137, 171, 205, 239, 1, 35, 69, 103, 137, 171, 205, 239, 1, 35, 69, 103,
188        137, 171, 205, 239, 1, 35, 69, 103, 137, 171, 205, 239,
189    ];
190    const IOTA_ADDRESS: IotaAddress = IotaAddress(ARR_ADDRESS);
191
192    #[test]
193    fn test_parse_valid_iotaaddress() {
194        let parsed = IotaAddress::from_str(STR_ADDRESS).unwrap();
195        assert_eq!(parsed.0, ARR_ADDRESS);
196    }
197
198    #[test]
199    fn test_to_value() {
200        let value = ScalarType::to_value(&IOTA_ADDRESS);
201        assert_eq!(value, Value::String(STR_ADDRESS.to_string()));
202    }
203
204    #[test]
205    fn test_from_array() {
206        let addr = IotaAddress::from_array(ARR_ADDRESS);
207        assert_eq!(addr, IOTA_ADDRESS);
208    }
209
210    #[test]
211    fn test_as_slice() {
212        assert_eq!(IOTA_ADDRESS.as_slice(), &ARR_ADDRESS);
213    }
214
215    #[test]
216    fn test_round_trip() {
217        let value = ScalarType::to_value(&IOTA_ADDRESS);
218        let parsed_back = ScalarType::parse(value).unwrap();
219        assert_eq!(IOTA_ADDRESS, parsed_back);
220    }
221
222    #[test]
223    fn test_parse_no_prefix() {
224        let err = IotaAddress::from_str(&STR_ADDRESS[2..]).unwrap_err();
225        assert_eq!(FromStrError::NoPrefix, err);
226    }
227
228    #[test]
229    fn test_parse_invalid_prefix() {
230        let input = "1x".to_string() + &STR_ADDRESS[2..];
231        let err = IotaAddress::from_str(&input).unwrap_err();
232        assert_eq!(FromStrError::NoPrefix, err)
233    }
234
235    #[test]
236    fn test_parse_invalid_length() {
237        let input = STR_ADDRESS.to_string() + "0123";
238        let err = IotaAddress::from_str(&input).unwrap_err();
239        assert_eq!(FromStrError::WrongLength(68), err)
240    }
241
242    #[test]
243    fn test_parse_invalid_characters() {
244        let input = "0xg".to_string() + &STR_ADDRESS[3..];
245        let err = IotaAddress::from_str(&input).unwrap_err();
246        assert_eq!(FromStrError::BadHex('g', 2), err);
247    }
248
249    #[test]
250    fn test_unicode_gibberish() {
251        let parsed = IotaAddress::from_str("aAௗ0㌀0");
252        assert!(parsed.is_err());
253    }
254
255    #[test]
256    fn bad_scalar_type() {
257        let input = Value::Number(0x42.into());
258        let parsed = <IotaAddress as ScalarType>::parse(input);
259        assert!(parsed.is_err());
260    }
261}