tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit 3532af5a9c0510a46a2d24af19c4f67c01c53b9a
parent 1400f1de58c2958840c12eb9cd80a80eb6082caf
Author: triesap <tyson@radroots.org>
Date:   Fri,  5 Jun 2026 20:26:31 -0700

protocol: add relay message encoder

Diffstat:
Mcrates/tangle_protocol/src/lib.rs | 172+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 168 insertions(+), 4 deletions(-)

diff --git a/crates/tangle_protocol/src/lib.rs b/crates/tangle_protocol/src/lib.rs @@ -460,6 +460,84 @@ pub enum ClientMessage { Auth(Event), } +#[derive(Debug, Clone, PartialEq)] +pub enum RelayMessage { + Event { + subscription_id: SubscriptionId, + event: Event, + }, + Ok { + event_id: EventId, + accepted: bool, + message: String, + }, + Eose(SubscriptionId), + Closed { + subscription_id: SubscriptionId, + message: String, + }, + Notice(String), + Auth(String), +} + +impl RelayMessage { + pub fn encode(&self) -> String { + encode_relay_message(self) + } +} + +pub fn encode_relay_message(message: &RelayMessage) -> String { + relay_message_to_value(message).to_string() +} + +pub fn relay_message_to_value(message: &RelayMessage) -> serde_json::Value { + match message { + RelayMessage::Event { + subscription_id, + event, + } => serde_json::json!(["EVENT", subscription_id.as_str(), event_to_value(event)]), + RelayMessage::Ok { + event_id, + accepted, + message, + } => serde_json::json!(["OK", event_id.as_str(), accepted, message]), + RelayMessage::Eose(subscription_id) => { + serde_json::json!(["EOSE", subscription_id.as_str()]) + } + RelayMessage::Closed { + subscription_id, + message, + } => serde_json::json!(["CLOSED", subscription_id.as_str(), message]), + RelayMessage::Notice(message) => serde_json::json!(["NOTICE", message]), + RelayMessage::Auth(challenge) => serde_json::json!(["AUTH", challenge]), + } +} + +pub fn event_to_value(event: &Event) -> serde_json::Value { + let tags = event + .unsigned() + .tags() + .iter() + .map(|tag| { + serde_json::Value::Array( + tag.values() + .iter() + .map(|value| serde_json::Value::String(value.clone())) + .collect(), + ) + }) + .collect::<Vec<_>>(); + serde_json::json!({ + "id": event.id().as_str(), + "pubkey": event.unsigned().pubkey().as_str(), + "created_at": event.unsigned().created_at().as_u64(), + "kind": event.unsigned().kind().as_u32(), + "tags": tags, + "content": event.unsigned().content(), + "sig": event.sig().as_str() + }) +} + pub fn parse_client_message(raw: &str) -> Result<ClientMessage, String> { let value: serde_json::Value = serde_json::from_str(raw) .map_err(|source| format!("client message JSON is invalid: {source}"))?; @@ -652,10 +730,10 @@ fn kind_out_of_range_error(value: u64) -> String { mod tests { use super::{ ClientMessage, Event, EventId, EventShapeError, Kind, PublicKeyHex, RawEventJson, - SignatureHex, SubscriptionId, Tag, TagName, TagValue, UnixTimestamp, UnsignedEvent, - canonical_event_json, empty_error, event_from_value, invalid_length_error, - kind_out_of_range_error, non_lowercase_hex_error, parse_client_message, parse_event_json, - too_long_error, + RelayMessage, SignatureHex, SubscriptionId, Tag, TagName, TagValue, UnixTimestamp, + UnsignedEvent, canonical_event_json, empty_error, encode_relay_message, event_from_value, + event_to_value, invalid_length_error, kind_out_of_range_error, non_lowercase_hex_error, + parse_client_message, parse_event_json, relay_message_to_value, too_long_error, }; use core::str::FromStr; use std::collections::hash_map::DefaultHasher; @@ -1122,6 +1200,92 @@ mod tests { } #[test] + fn relay_message_encoder_emits_nip01_and_nip42_messages() { + let event = parse_event_json( + &RawEventJson::new(&event_json("a", "b", 1, tags_json())).expect("raw"), + ) + .expect("event"); + let subscription_id = SubscriptionId::new("sub-a").expect("sub"); + let accepted_id = EventId::new(&"c".repeat(EventId::HEX_LENGTH)).expect("id"); + let rejected_id = EventId::new(&"d".repeat(EventId::HEX_LENGTH)).expect("id"); + + assert_eq!( + relay_message_to_value(&RelayMessage::Event { + subscription_id: subscription_id.clone(), + event: event.clone() + }), + serde_json::json!(["EVENT", "sub-a", event_to_value(&event)]) + ); + assert_eq!( + serde_json::from_str::<serde_json::Value>(&encode_relay_message(&RelayMessage::Ok { + event_id: accepted_id, + accepted: true, + message: String::new() + })) + .expect("ok"), + serde_json::json!([ + "OK", + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + true, + "" + ]) + ); + assert_eq!( + serde_json::from_str::<serde_json::Value>( + &RelayMessage::Ok { + event_id: rejected_id, + accepted: false, + message: "invalid: event id mismatch".to_owned() + } + .encode() + ) + .expect("rejected"), + serde_json::json!([ + "OK", + "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + false, + "invalid: event id mismatch" + ]) + ); + assert_eq!( + relay_message_to_value(&RelayMessage::Eose(subscription_id.clone())), + serde_json::json!(["EOSE", "sub-a"]) + ); + assert_eq!( + relay_message_to_value(&RelayMessage::Closed { + subscription_id, + message: "unsupported: filter contains unknown elements".to_owned() + }), + serde_json::json!([ + "CLOSED", + "sub-a", + "unsupported: filter contains unknown elements" + ]) + ); + assert_eq!( + relay_message_to_value(&RelayMessage::Notice("maintenance window".to_owned())), + serde_json::json!(["NOTICE", "maintenance window"]) + ); + assert_eq!( + relay_message_to_value(&RelayMessage::Auth("challenge-a".to_owned())), + serde_json::json!(["AUTH", "challenge-a"]) + ); + } + + #[test] + fn event_to_value_round_trips_with_event_parser() { + let event = parse_event_json( + &RawEventJson::new(&event_json("e", "f", 30402, tags_json())).expect("raw"), + ) + .expect("event"); + + assert_eq!( + event_from_value(&event_to_value(&event)).expect("parsed"), + event + ); + } + + #[test] fn canonical_event_json_serializes_empty_content_and_tags() { let event = unsigned_event(Vec::new(), "");