iota_graphql_rpc/types/
iota_address.rs1use 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 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
169pub(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}