sui_rpc/light_client/events/
envelope.rs1use sui_sdk_types::Digest;
5use sui_sdk_types::Event;
6
7use crate::proto::TryFromProtoError;
8use crate::proto::sui::rpc::v2::Event as ProtoEvent;
9use crate::proto::sui::rpc::v2::ListEventsResponse;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
29pub struct AuthenticatedEvent {
30 pub checkpoint: u64,
32 pub transaction_index: u64,
35 pub event_index: u32,
37 pub transaction_digest: Digest,
39 pub event: Event,
41}
42
43impl TryFrom<&ListEventsResponse> for AuthenticatedEvent {
44 type Error = TryFromProtoError;
45
46 fn try_from(value: &ListEventsResponse) -> Result<Self, Self::Error> {
47 let event_proto = value
48 .event
49 .as_ref()
50 .ok_or_else(|| TryFromProtoError::missing(ListEventsResponse::EVENT_FIELD.name))?;
51
52 let missing = |field: &'static str| {
56 TryFromProtoError::missing(field).nested(ListEventsResponse::EVENT_FIELD.name)
57 };
58
59 let checkpoint = event_proto
60 .checkpoint
61 .ok_or_else(|| missing(ProtoEvent::CHECKPOINT_FIELD.name))?;
62 let transaction_index = event_proto
63 .transaction_index
64 .ok_or_else(|| missing(ProtoEvent::TRANSACTION_INDEX_FIELD.name))?;
65 let event_index = event_proto
66 .event_index
67 .ok_or_else(|| missing(ProtoEvent::EVENT_INDEX_FIELD.name))?;
68 let transaction_digest = event_proto
69 .transaction_digest
70 .as_ref()
71 .ok_or_else(|| missing(ProtoEvent::TRANSACTION_DIGEST_FIELD.name))?
72 .parse()
73 .map_err(|e| {
74 TryFromProtoError::invalid(ProtoEvent::TRANSACTION_DIGEST_FIELD, e)
75 .nested(ListEventsResponse::EVENT_FIELD.name)
76 })?;
77 let event = Event::try_from(event_proto)
78 .map_err(|e| e.nested(ListEventsResponse::EVENT_FIELD.name))?;
79
80 Ok(Self {
81 checkpoint,
82 transaction_index,
83 event_index,
84 transaction_digest,
85 event,
86 })
87 }
88}
89
90impl From<&AuthenticatedEvent> for ListEventsResponse {
91 fn from(value: &AuthenticatedEvent) -> Self {
92 let event = ProtoEvent::from(value.event.clone())
93 .with_checkpoint(value.checkpoint)
94 .with_transaction_digest(value.transaction_digest.to_string())
95 .with_transaction_index(value.transaction_index)
96 .with_event_index(value.event_index);
97 Self {
98 event: Some(event),
99 watermark: None,
102 end: None,
103 }
104 }
105}
106
107impl From<AuthenticatedEvent> for ListEventsResponse {
108 fn from(value: AuthenticatedEvent) -> Self {
109 (&value).into()
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116 use sui_sdk_types::Address;
117 use sui_sdk_types::Identifier;
118 use sui_sdk_types::StructTag;
119
120 fn sample_event() -> Event {
121 Event {
122 package_id: Address::TWO,
123 module: Identifier::from_static("clock"),
124 sender: Address::TWO,
125 type_: StructTag::new(
126 Address::TWO,
127 Identifier::from_static("clock"),
128 Identifier::from_static("Tick"),
129 vec![],
130 ),
131 contents: vec![0xde, 0xad, 0xbe, 0xef],
132 }
133 }
134
135 fn sample_authenticated_event() -> AuthenticatedEvent {
136 AuthenticatedEvent {
137 checkpoint: 42,
138 transaction_index: 3,
139 event_index: 7,
140 transaction_digest: Digest::new([0xaa; 32]),
141 event: sample_event(),
142 }
143 }
144
145 #[test]
146 fn round_trip_through_proto_preserves_envelope() {
147 let original = sample_authenticated_event();
148 let proto: ListEventsResponse = (&original).into();
149 let back = AuthenticatedEvent::try_from(&proto).unwrap();
150 assert_eq!(back, original);
151 }
152
153 #[test]
154 fn outbound_conversion_leaves_server_fields_unset() {
155 let proto: ListEventsResponse = (&sample_authenticated_event()).into();
156 assert!(
157 proto.watermark.is_none(),
158 "watermark (cursor + checkpoint) must be server-assigned"
159 );
160 assert!(proto.end.is_none(), "end must be server-assigned");
161 }
162
163 #[test]
164 fn missing_checkpoint_is_rejected() {
165 let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
166 proto.event.as_mut().unwrap().checkpoint = None;
167 let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
168 assert_eq!(err.field_violation().field, "event.checkpoint");
169 }
170
171 #[test]
172 fn missing_transaction_index_is_rejected() {
173 let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
174 proto.event.as_mut().unwrap().transaction_index = None;
175 let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
176 assert_eq!(err.field_violation().field, "event.transaction_index");
177 }
178
179 #[test]
180 fn missing_event_index_is_rejected() {
181 let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
182 proto.event.as_mut().unwrap().event_index = None;
183 let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
184 assert_eq!(err.field_violation().field, "event.event_index");
185 }
186
187 #[test]
188 fn missing_transaction_digest_is_rejected() {
189 let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
190 proto.event.as_mut().unwrap().transaction_digest = None;
191 let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
192 assert_eq!(err.field_violation().field, "event.transaction_digest");
193 }
194
195 #[test]
196 fn missing_event_is_rejected() {
197 let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
198 proto.event = None;
199 let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
200 assert_eq!(err.field_violation().field, "event");
201 }
202
203 #[test]
207 fn malformed_inner_event_reports_nested_field_path() {
208 let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
209 proto.event.as_mut().unwrap().package_id = None;
211 let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
212 let path = &err.field_violation().field;
213 assert!(
214 path.starts_with("event."),
215 "field path should be nested under the parent `event` field, got {path}",
216 );
217 }
218
219 #[test]
222 fn malformed_transaction_digest_is_rejected() {
223 let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
224 proto.event.as_mut().unwrap().transaction_digest = Some("not-a-real-digest!".into());
225 let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
226 assert_eq!(err.field_violation().field, "event.transaction_digest");
227 }
228}