//! Waku [general](https://rfc.vac.dev/spec/36/#general) types // std use std::borrow::Cow; use std::fmt::{Display, Formatter}; use std::str::FromStr; // crates use aes_gcm::{Aes256Gcm, Key}; use base64::Engine; use secp256k1::{ecdsa::Signature, PublicKey, SecretKey}; use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; use sscanf::{scanf, RegexRepresentation}; // internal use crate::decrypt::{waku_decode_asymmetric, waku_decode_symmetric}; /// Waku message version pub type WakuMessageVersion = usize; /// Base58 encoded peer id pub type PeerId = String; /// Waku message id, hex encoded sha256 digest of the message pub type MessageId = String; /// Protocol identifiers #[non_exhaustive] #[derive(Debug, Clone, Copy)] pub enum ProtocolId { Store, Lightpush, Filter, Relay, } impl ProtocolId { pub fn as_string_with_version(&self, version: &str) -> String { format!("{self}/{version}") } } impl Display for ProtocolId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let tag = match self { ProtocolId::Store => "/vac/waku/store", ProtocolId::Lightpush => "/vac/waku/lightpush", ProtocolId::Filter => "/vac/waku/filter", ProtocolId::Relay => "/vac/waku/relay", #[allow(unreachable_patterns)] _ => unreachable!(), }; write!(f, "{tag}") } } /// JsonResponse wrapper. /// `go-waku` ffi returns this type as a `char *` as per the [specification](https://rfc.vac.dev/spec/36/#jsonresponse-type) /// This is internal, as it is better to use rust plain `Result` type. #[derive(Deserialize, Debug)] #[serde(rename_all = "snake_case")] pub(crate) enum JsonResponse { Result(T), Error(String), } /// Waku response, just a `Result` with an `String` error. pub type Result = std::result::Result; /// Convenient we can transform a [`JsonResponse`] into a [`std::result::Result`] impl From> for Result { fn from(response: JsonResponse) -> Self { match response { JsonResponse::Result(t) => Ok(t), JsonResponse::Error(e) => Err(e), } } } // TODO: Properly type and deserialize payload form base64 encoded string /// Waku message in JSON format. /// as per the [specification](https://rfc.vac.dev/spec/36/#jsonmessage-type) #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct WakuMessage { #[serde(with = "base64_serde")] payload: Vec, /// The content topic to be set on the message content_topic: WakuContentTopic, // TODO: check if missing default should be 0 /// The Waku Message version number #[serde(default)] version: WakuMessageVersion, /// Unix timestamp in nanoseconds timestamp: usize, // TODO: implement RLN fields #[serde(flatten)] _extras: serde_json::Value, } impl WakuMessage { pub fn new>( payload: PAYLOAD, content_topic: WakuContentTopic, version: WakuMessageVersion, timestamp: usize, ) -> Self { let payload = payload.as_ref().to_vec(); Self { payload, content_topic, version, timestamp, _extras: Default::default(), } } pub fn payload(&self) -> &[u8] { &self.payload } pub fn content_topic(&self) -> &WakuContentTopic { &self.content_topic } pub fn version(&self) -> WakuMessageVersion { self.version } pub fn timestamp(&self) -> usize { self.timestamp } /// Try decode the message with an expected symmetric key /// /// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_decode_symmetricchar-messagejson-char-symmetrickey) pub fn try_decode_symmetric(&self, symmetric_key: &Key) -> Result { waku_decode_symmetric(self, symmetric_key) } /// Try decode the message with an expected asymmetric key /// /// As per the [specification](https://rfc.vac.dev/spec/36/#extern-char-waku_decode_asymmetricchar-messagejson-char-privatekey) pub fn try_decode_asymmetric(&self, asymmetric_key: &SecretKey) -> Result { waku_decode_asymmetric(self, asymmetric_key) } } /// A payload once decoded, used when a received Waku Message is encrypted #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct DecodedPayload { /// Public key that signed the message (optional), hex encoded with 0x prefix #[serde(deserialize_with = "deserialize_optional_pk", default)] public_key: Option, /// Message signature (optional), hex encoded with 0x prefix #[serde(deserialize_with = "deserialize_optional_signature", default)] signature: Option, /// Decrypted message payload base64 encoded #[serde(with = "base64_serde")] data: Vec, /// Padding base64 encoded #[serde(with = "base64_serde")] padding: Vec, } impl DecodedPayload { pub fn public_key(&self) -> Option<&PublicKey> { self.public_key.as_ref() } pub fn signature(&self) -> Option<&Signature> { self.signature.as_ref() } pub fn data(&self) -> &[u8] { &self.data } pub fn padding(&self) -> &[u8] { &self.padding } } /// The content topic of a Waku message /// as per the [specification](https://rfc.vac.dev/spec/36/#contentfilter-type) #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct ContentFilter { /// The content topic of a Waku message content_topic: WakuContentTopic, } impl ContentFilter { pub fn new(content_topic: WakuContentTopic) -> Self { Self { content_topic } } pub fn content_topic(&self) -> &WakuContentTopic { &self.content_topic } } /// The criteria to create subscription to a light node in JSON Format /// as per the [specification](https://rfc.vac.dev/spec/36/#filtersubscription-type) #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct FilterSubscription { /// Array of [`ContentFilter`] being subscribed to / unsubscribed from content_filters: Vec, /// Optional pubsub topic pubsub_topic: Option, } impl FilterSubscription { pub fn new(content_filters: Vec, pubsub_topic: Option) -> Self { Self { content_filters, pubsub_topic, } } pub fn content_filters(&self) -> &[ContentFilter] { &self.content_filters } pub fn pubsub_topic(&self) -> Option<&WakuPubSubTopic> { self.pubsub_topic.as_ref() } } /// Criteria used to retrieve historical messages #[derive(Clone, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct StoreQuery { /// The pubsub topic on which messages are published pub pubsub_topic: Option, /// Array of [`ContentFilter`] to query for historical messages pub content_filters: Vec, /// The inclusive lower bound on the timestamp of queried messages. /// This field holds the Unix epoch time in nanoseconds pub start_time: Option, /// The inclusive upper bound on the timestamp of queried messages. /// This field holds the Unix epoch time in nanoseconds pub end_time: Option, /// Paging information in [`PagingOptions`] format pub paging_options: Option, } /// The response received after doing a query to a store node #[derive(Clone, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct StoreResponse { /// Array of retrieved historical messages in [`WakuMessage`] format #[serde(default)] pub messages: Vec, /// Paging information in [`PagingOptions`] format from which to resume further historical queries pub paging_options: Option, } impl StoreResponse { pub fn messages(&self) -> &[WakuMessage] { &self.messages } pub fn paging_options(&self) -> Option<&PagingOptions> { self.paging_options.as_ref() } } /// Paging information #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct PagingOptions { /// Number of messages to retrieve per page pub page_size: usize, /// Message Index from which to perform pagination. /// If not included and forward is set to `true`, paging will be performed from the beginning of the list. /// If not included and forward is set to `false`, paging will be performed from the end of the list pub cursor: Option, /// `true` if paging forward, `false` if paging backward pub forward: bool, } /// Pagination index type #[derive(Clone, Serialize, Deserialize, Debug)] #[serde(rename_all = "camelCase")] pub struct MessageIndex { /// Hash of the message at this [``MessageIndex`] pub digest: String, /// UNIX timestamp in nanoseconds at which the message at this [`MessageIndex`] was received pub receiver_time: usize, /// UNIX timestamp in nanoseconds at which the message is generated by its sender pub sender_time: usize, /// The pubsub topic of the message at this [`MessageIndex`] pub pubsub_topic: WakuPubSubTopic, } /// WakuMessage encoding scheme #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum Encoding { Proto, Rlp, Rfc26, } impl Display for Encoding { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { let s = match self { Encoding::Proto => "proto", Encoding::Rlp => "rlp", Encoding::Rfc26 => "rfc26", }; f.write_str(s) } } impl FromStr for Encoding { type Err = std::io::Error; fn from_str(s: &str) -> std::result::Result { match s.to_lowercase().as_str() { "proto" => Ok(Self::Proto), "rlp" => Ok(Self::Rlp), "rfc26" => Ok(Self::Rfc26), encoding => Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, format!("Unrecognized encoding: {encoding}"), )), } } } impl RegexRepresentation for Encoding { const REGEX: &'static str = r"\w"; } /// A waku content topic `/{application_name}/{version}/{content_topic_name}/{encdoing}` #[derive(Clone, Debug, Eq, PartialEq)] pub struct WakuContentTopic { pub application_name: Cow<'static, str>, pub version: usize, pub content_topic_name: Cow<'static, str>, pub encoding: Encoding, } impl WakuContentTopic { pub const fn new( application_name: &'static str, version: usize, content_topic_name: &'static str, encoding: Encoding, ) -> Self { Self { application_name: Cow::Borrowed(application_name), version, content_topic_name: Cow::Borrowed(content_topic_name), encoding, } } } impl FromStr for WakuContentTopic { type Err = String; fn from_str(s: &str) -> std::result::Result { if let Ok((application_name, version, content_topic_name, encoding)) = scanf!(s, "/{}/{}/{}/{:/.+?/}", String, usize, String, Encoding) { Ok(WakuContentTopic { application_name: Cow::Owned(application_name), version, content_topic_name: Cow::Owned(content_topic_name), encoding, }) } else { Err( format!( "Wrong pub-sub topic format. Should be `/{{application-name}}/{{version-of-the-application}}/{{content-topic-name}}/{{encoding}}`. Got: {s}" ) ) } } } impl Display for WakuContentTopic { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!( f, "/{}/{}/{}/{}", self.application_name, self.version, self.content_topic_name, self.encoding ) } } impl Serialize for WakuContentTopic { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { self.to_string().serialize(serializer) } } impl<'de> Deserialize<'de> for WakuContentTopic { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { let as_string: String = String::deserialize(deserializer)?; as_string .parse::() .map_err(D::Error::custom) } } /// A waku pubsub topic in the form of `/waku/v2/{topic_name}/{encoding}` #[derive(Clone, Debug, Eq, PartialEq)] pub struct WakuPubSubTopic { pub topic_name: Cow<'static, str>, pub encoding: Encoding, } impl WakuPubSubTopic { pub const fn new(topic_name: &'static str, encoding: Encoding) -> Self { Self { topic_name: Cow::Borrowed(topic_name), encoding, } } pub fn with_topic_name(topic_name: String, encoding: Encoding) -> Self { Self { topic_name: Cow::Owned(topic_name), encoding, } } } impl FromStr for WakuPubSubTopic { type Err = String; fn from_str(s: &str) -> std::result::Result { if let Ok((topic_name, encoding)) = scanf!(s, "/waku/2/{}/{:/.+?/}", String, Encoding) { Ok(WakuPubSubTopic { topic_name: Cow::Owned(topic_name), encoding, }) } else { Err( format!( "Wrong pub-sub topic format. Should be `/waku/2/{{topic-name}}/{{encoding}}`. Got: {s}" ) ) } } } impl Display for WakuPubSubTopic { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { write!(f, "/waku/2/{}/{}", self.topic_name, self.encoding) } } impl Serialize for WakuPubSubTopic { fn serialize(&self, serializer: S) -> std::result::Result where S: Serializer, { self.to_string().serialize(serializer) } } impl<'de> Deserialize<'de> for WakuPubSubTopic { fn deserialize(deserializer: D) -> std::result::Result where D: Deserializer<'de>, { let as_string: String = String::deserialize(deserializer)?; as_string .parse::() .map_err(D::Error::custom) } } mod base64_serde { use base64::Engine; use serde::de::Error; use serde::{Deserialize, Deserializer, Serialize, Serializer}; pub fn serialize(value: &[u8], serializer: S) -> std::result::Result where S: Serializer, { base64::engine::general_purpose::STANDARD .encode(value) .serialize(serializer) } pub fn deserialize<'de, D>(deserializer: D) -> std::result::Result, D::Error> where D: Deserializer<'de>, { let base64_str: String = String::deserialize(deserializer)?; base64::engine::general_purpose::STANDARD .decode(base64_str) .map_err(D::Error::custom) } } pub fn deserialize_optional_pk<'de, D>( deserializer: D, ) -> std::result::Result, D::Error> where D: Deserializer<'de>, { let base64_str: Option = Option::::deserialize(deserializer)?; base64_str .map(|base64_str| { let raw_bytes = base64::engine::general_purpose::STANDARD .decode(base64_str) .map_err(D::Error::custom)?; PublicKey::from_slice(&raw_bytes).map_err(D::Error::custom) }) .transpose() } pub fn deserialize_optional_signature<'de, D>( deserializer: D, ) -> std::result::Result, D::Error> where D: Deserializer<'de>, { let hex_str: Option = Option::::deserialize(deserializer)?; hex_str .map(|hex_str| { let raw_bytes = hex::decode(hex_str.strip_prefix("0x").unwrap_or(&hex_str)) .map_err(D::Error::custom)?; if ![64, 65].contains(&raw_bytes.len()) { return Err(D::Error::custom( "Invalid signature, only 64 or 65 bytes len are supported", )); } Signature::from_compact(&raw_bytes[..64]).map_err(D::Error::custom) }) .transpose() } #[cfg(test)] mod tests { use super::*; use crate::WakuPubSubTopic; #[test] fn parse_waku_topic() { let s = "/waku/2/default-waku/proto"; let _: WakuPubSubTopic = s.parse().unwrap(); } #[test] fn deserialize_waku_message() { let message = "{\"payload\":\"SGkgZnJvbSDwn6aAIQ==\",\"contentTopic\":\"/toychat/2/huilong/proto\",\"timestamp\":1665580926660}"; let _: WakuMessage = serde_json::from_str(message).unwrap(); } }