iota_grpc_types/proto/
mod.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2025 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5#![allow(clippy::large_enum_variant)]
6#![allow(clippy::doc_overindented_list_items)]
7#![allow(clippy::module_inception)]
8
9use google::rpc::bad_request::FieldViolation;
10use iota::grpc::v1::error_reason::ErrorReason;
11
12pub(crate) mod google;
13pub(crate) mod iota;
14
15pub type BoxError = Box<dyn std::error::Error + Send + Sync + 'static>;
16
17#[derive(Debug)]
18pub struct TryFromProtoError {
19    field_violation: FieldViolation,
20    source: Option<BoxError>,
21}
22
23impl std::fmt::Display for TryFromProtoError {
24    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
25        write!(f, "error converting from protobuf: ")?;
26
27        write!(f, "field: {}", self.field_violation.field)?;
28
29        if !self.field_violation.reason.is_empty() {
30            write!(f, " reason: {}", self.field_violation.reason)?;
31        }
32
33        if !self.field_violation.description.is_empty() {
34            write!(f, " description: {}", self.field_violation.description)?;
35        }
36
37        Ok(())
38    }
39}
40
41impl std::error::Error for TryFromProtoError {
42    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
43        self.source.as_deref().map(|s| s as _)
44    }
45}
46
47impl TryFromProtoError {
48    pub fn nested<T: AsRef<str>>(mut self, field: T) -> Self {
49        let field = field.as_ref();
50        self.field_violation = self.field_violation.nested(field);
51        self
52    }
53
54    pub fn nested_at<T: AsRef<str>>(mut self, field: T, index: usize) -> Self {
55        let field = field.as_ref();
56        self.field_violation = self.field_violation.nested_at(field, index);
57        self
58    }
59
60    pub fn missing<T: AsRef<str>>(field: T) -> Self {
61        let field = field.as_ref();
62
63        Self {
64            field_violation: FieldViolation::new(field).with_reason(ErrorReason::FieldMissing),
65            source: None,
66        }
67    }
68
69    pub fn invalid<T: AsRef<str>, E: Into<BoxError>>(field: T, error: E) -> Self {
70        let field = field.as_ref();
71        let error = error.into();
72
73        Self {
74            field_violation: FieldViolation::new(field)
75                .with_reason(ErrorReason::FieldInvalid)
76                .with_description(error.to_string()),
77            source: Some(error),
78        }
79    }
80
81    pub fn field_violation(&self) -> &FieldViolation {
82        &self.field_violation
83    }
84}
85
86/// Macro to reduce boilerplate when accessing an optional field and calling
87/// an inner method that returns `Result<T, TryFromProtoError>`.
88///
89/// # Usage
90/// ```ignore
91/// get_inner_field!(self.transaction, Self::TRANSACTION_FIELD, digest)
92/// ```
93macro_rules! get_inner_field {
94    // Variant for try_into() that needs explicit TryFromProtoError type annotation
95    // This must come first to match before the general case
96    ($field:expr, $FIELD:expr, try_into) => {{
97        <_ as core::convert::TryInto<_>>::try_into(
98            $field
99                .as_ref()
100                .ok_or_else(|| $crate::proto::TryFromProtoError::missing($FIELD.name))?,
101        )
102        .map_err(|e: $crate::proto::TryFromProtoError| e.nested($FIELD.name))
103    }};
104    // Standard case: call a method on the inner value
105    ($field:expr, $FIELD:expr, $inner:ident) => {{
106        $field
107            .as_ref()
108            .ok_or_else(|| $crate::proto::TryFromProtoError::missing($FIELD.name))?
109            .$inner()
110            .map_err(|e| e.nested($FIELD.name))
111    }};
112}
113
114pub(crate) use get_inner_field;
115
116#[derive(Debug)]
117pub enum GrpcConversionError {
118    UnsupportedArgumentType { arg_type: String },
119    BcsSerializationFailed { message: String },
120}
121
122impl std::fmt::Display for GrpcConversionError {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        match self {
125            Self::UnsupportedArgumentType { arg_type } => {
126                write!(
127                    f,
128                    "Unsupported argument type for gRPC conversion: {}",
129                    arg_type
130                )
131            }
132            Self::BcsSerializationFailed { message } => {
133                write!(f, "Failed to serialize BCS data: {}", message)
134            }
135        }
136    }
137}
138
139impl std::error::Error for GrpcConversionError {}
140
141// TimeStamp
142//
143
144pub fn timestamp_ms_to_proto(timestamp_ms: u64) -> prost_types::Timestamp {
145    let timestamp = std::time::Duration::from_millis(timestamp_ms);
146    prost_types::Timestamp {
147        seconds: timestamp.as_secs() as i64,
148        nanos: timestamp.subsec_nanos() as i32,
149    }
150}
151
152#[allow(clippy::result_large_err)]
153pub fn proto_to_timestamp_ms(timestamp: prost_types::Timestamp) -> Result<u64, TryFromProtoError> {
154    let seconds = std::time::Duration::from_secs(
155        timestamp
156            .seconds
157            .try_into()
158            .map_err(|e| TryFromProtoError::invalid("seconds", e))?,
159    );
160    let nanos = std::time::Duration::from_nanos(
161        timestamp
162            .nanos
163            .try_into()
164            .map_err(|e| TryFromProtoError::invalid("nanos", e))?,
165    );
166
167    (seconds + nanos)
168        .as_millis()
169        .try_into()
170        .map_err(|e| TryFromProtoError::invalid("seconds + nanos", e))
171}
172
173// prost_types::Value to serde_json::Value conversion
174//
175
176/// Converts a prost_types::Value to serde_json::Value.
177pub fn prost_to_json(value: &prost_types::Value) -> serde_json::Value {
178    use prost_types::value::Kind;
179
180    match &value.kind {
181        None => serde_json::Value::Null,
182        Some(Kind::NullValue(_)) => serde_json::Value::Null,
183        Some(Kind::NumberValue(n)) => serde_json::json!(*n),
184        Some(Kind::StringValue(s)) => serde_json::Value::String(s.clone()),
185        Some(Kind::BoolValue(b)) => serde_json::Value::Bool(*b),
186        Some(Kind::StructValue(s)) => {
187            let map: serde_json::Map<String, serde_json::Value> = s
188                .fields
189                .iter()
190                .map(|(k, v)| (k.clone(), prost_to_json(v)))
191                .collect();
192            serde_json::Value::Object(map)
193        }
194        Some(Kind::ListValue(l)) => {
195            let arr: Vec<serde_json::Value> = l.values.iter().map(prost_to_json).collect();
196            serde_json::Value::Array(arr)
197        }
198    }
199}