use sui_sdk_types::Digest;
use sui_sdk_types::Event;
use crate::proto::TryFromProtoError;
use crate::proto::sui::rpc::v2::Event as ProtoEvent;
use crate::proto::sui::rpc::v2::ListEventsResponse;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct AuthenticatedEvent {
pub checkpoint: u64,
pub transaction_index: u64,
pub event_index: u32,
pub transaction_digest: Digest,
pub event: Event,
}
impl TryFrom<&ListEventsResponse> for AuthenticatedEvent {
type Error = TryFromProtoError;
fn try_from(value: &ListEventsResponse) -> Result<Self, Self::Error> {
let event_proto = value
.event
.as_ref()
.ok_or_else(|| TryFromProtoError::missing(ListEventsResponse::EVENT_FIELD.name))?;
let missing = |field: &'static str| {
TryFromProtoError::missing(field).nested(ListEventsResponse::EVENT_FIELD.name)
};
let checkpoint = event_proto
.checkpoint
.ok_or_else(|| missing(ProtoEvent::CHECKPOINT_FIELD.name))?;
let transaction_index = event_proto
.transaction_index
.ok_or_else(|| missing(ProtoEvent::TRANSACTION_INDEX_FIELD.name))?;
let event_index = event_proto
.event_index
.ok_or_else(|| missing(ProtoEvent::EVENT_INDEX_FIELD.name))?;
let transaction_digest = event_proto
.transaction_digest
.as_ref()
.ok_or_else(|| missing(ProtoEvent::TRANSACTION_DIGEST_FIELD.name))?
.parse()
.map_err(|e| {
TryFromProtoError::invalid(ProtoEvent::TRANSACTION_DIGEST_FIELD, e)
.nested(ListEventsResponse::EVENT_FIELD.name)
})?;
let event = Event::try_from(event_proto)
.map_err(|e| e.nested(ListEventsResponse::EVENT_FIELD.name))?;
Ok(Self {
checkpoint,
transaction_index,
event_index,
transaction_digest,
event,
})
}
}
impl From<&AuthenticatedEvent> for ListEventsResponse {
fn from(value: &AuthenticatedEvent) -> Self {
let event = ProtoEvent::from(value.event.clone())
.with_checkpoint(value.checkpoint)
.with_transaction_digest(value.transaction_digest.to_string())
.with_transaction_index(value.transaction_index)
.with_event_index(value.event_index);
Self {
event: Some(event),
watermark: None,
end: None,
}
}
}
impl From<AuthenticatedEvent> for ListEventsResponse {
fn from(value: AuthenticatedEvent) -> Self {
(&value).into()
}
}
#[cfg(test)]
mod tests {
use super::*;
use sui_sdk_types::Address;
use sui_sdk_types::Identifier;
use sui_sdk_types::StructTag;
fn sample_event() -> Event {
Event {
package_id: Address::TWO,
module: Identifier::from_static("clock"),
sender: Address::TWO,
type_: StructTag::new(
Address::TWO,
Identifier::from_static("clock"),
Identifier::from_static("Tick"),
vec![],
),
contents: vec![0xde, 0xad, 0xbe, 0xef],
}
}
fn sample_authenticated_event() -> AuthenticatedEvent {
AuthenticatedEvent {
checkpoint: 42,
transaction_index: 3,
event_index: 7,
transaction_digest: Digest::new([0xaa; 32]),
event: sample_event(),
}
}
#[test]
fn round_trip_through_proto_preserves_envelope() {
let original = sample_authenticated_event();
let proto: ListEventsResponse = (&original).into();
let back = AuthenticatedEvent::try_from(&proto).unwrap();
assert_eq!(back, original);
}
#[test]
fn outbound_conversion_leaves_server_fields_unset() {
let proto: ListEventsResponse = (&sample_authenticated_event()).into();
assert!(
proto.watermark.is_none(),
"watermark (cursor + checkpoint) must be server-assigned"
);
assert!(proto.end.is_none(), "end must be server-assigned");
}
#[test]
fn missing_checkpoint_is_rejected() {
let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
proto.event.as_mut().unwrap().checkpoint = None;
let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
assert_eq!(err.field_violation().field, "event.checkpoint");
}
#[test]
fn missing_transaction_index_is_rejected() {
let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
proto.event.as_mut().unwrap().transaction_index = None;
let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
assert_eq!(err.field_violation().field, "event.transaction_index");
}
#[test]
fn missing_event_index_is_rejected() {
let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
proto.event.as_mut().unwrap().event_index = None;
let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
assert_eq!(err.field_violation().field, "event.event_index");
}
#[test]
fn missing_transaction_digest_is_rejected() {
let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
proto.event.as_mut().unwrap().transaction_digest = None;
let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
assert_eq!(err.field_violation().field, "event.transaction_digest");
}
#[test]
fn missing_event_is_rejected() {
let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
proto.event = None;
let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
assert_eq!(err.field_violation().field, "event");
}
#[test]
fn malformed_inner_event_reports_nested_field_path() {
let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
proto.event.as_mut().unwrap().package_id = None;
let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
let path = &err.field_violation().field;
assert!(
path.starts_with("event."),
"field path should be nested under the parent `event` field, got {path}",
);
}
#[test]
fn malformed_transaction_digest_is_rejected() {
let mut proto: ListEventsResponse = (&sample_authenticated_event()).into();
proto.event.as_mut().unwrap().transaction_digest = Some("not-a-real-digest!".into());
let err = AuthenticatedEvent::try_from(&proto).unwrap_err();
assert_eq!(err.field_violation().field, "event.transaction_digest");
}
}