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