From 3c914c725a5401706c33affb6cad8298af085fac Mon Sep 17 00:00:00 2001 From: Chirantan Ekbote Date: Fri, 6 Jan 2023 17:49:45 +0900 Subject: [PATCH] cosmwasm: Add cw_transcode crate The cw_transcode crate provides a way to transcode any arbitrary rust struct into a `cosmwasm_std::Event` via that struct's `Serialize` impl, ensuring that the event attribute values are encoded as proper json. This will make it easier for client code to parse the event back into structured data without having to write custom parsing code for each individual event type. --- cosmwasm/Cargo.lock | 15 + cosmwasm/Cargo.toml | 2 + cosmwasm/packages/cw_transcode/Cargo.toml | 15 + cosmwasm/packages/cw_transcode/src/error.rs | 26 ++ cosmwasm/packages/cw_transcode/src/lib.rs | 285 +++++++++++++++++ cosmwasm/packages/cw_transcode/src/ser.rs | 328 ++++++++++++++++++++ 6 files changed, 671 insertions(+) create mode 100644 cosmwasm/packages/cw_transcode/Cargo.toml create mode 100644 cosmwasm/packages/cw_transcode/src/error.rs create mode 100644 cosmwasm/packages/cw_transcode/src/lib.rs create mode 100644 cosmwasm/packages/cw_transcode/src/ser.rs diff --git a/cosmwasm/Cargo.lock b/cosmwasm/Cargo.lock index 761cf13af..9cedd5797 100644 --- a/cosmwasm/Cargo.lock +++ b/cosmwasm/Cargo.lock @@ -594,6 +594,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "cw_transcode" +version = "0.1.0" +dependencies = [ + "cosmwasm-std", + "hex", + "serde", + "serde-json-wasm", + "serde_bytes", + "thiserror", +] + [[package]] name = "darling" version = "0.14.2" @@ -967,6 +979,9 @@ name = "hex" version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] [[package]] name = "hmac" diff --git a/cosmwasm/Cargo.toml b/cosmwasm/Cargo.toml index 32edc160f..a1b386c15 100644 --- a/cosmwasm/Cargo.toml +++ b/cosmwasm/Cargo.toml @@ -9,6 +9,7 @@ members = [ "packages/accounting", "contracts/wormchain-accounting", "packages/wormhole-bindings", + "packages/cw_transcode", ] # Needed to prevent unwanted feature unification between normal builds and dev builds. See @@ -28,6 +29,7 @@ overflow-checks = true [patch.crates-io] accounting = { path = "packages/accounting" } +cw_transcode = { path = "packages/cw_transcode" } cw20-wrapped-2 = { path = "contracts/cw20-wrapped" } serde_wormhole = { path = "../sdk/rust/serde_wormhole" } token-bridge-terra-2 = { path = "contracts/token-bridge" } diff --git a/cosmwasm/packages/cw_transcode/Cargo.toml b/cosmwasm/packages/cw_transcode/Cargo.toml new file mode 100644 index 000000000..724027b2e --- /dev/null +++ b/cosmwasm/packages/cw_transcode/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "cw_transcode" +version = "0.1.0" +authors = ["Wormhole Project Contributors"] +edition = "2021" + +[dependencies] +cosmwasm-std = "1" +serde = { version = "1", default-features = false } +serde-json-wasm = "0.4" +thiserror = "1" + +[dev-dependencies] +hex = { version = "0.4", features = ["serde"] } +serde_bytes = "0.11" diff --git a/cosmwasm/packages/cw_transcode/src/error.rs b/cosmwasm/packages/cw_transcode/src/error.rs new file mode 100644 index 000000000..59f972bf2 --- /dev/null +++ b/cosmwasm/packages/cw_transcode/src/error.rs @@ -0,0 +1,26 @@ +use std::fmt; + +use serde::ser; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("{0}")] + Custom(Box), + #[error("cannot transcode non-struct type to `Event`")] + NotAStruct, + #[error("{0}")] + Json(#[from] serde_json_wasm::ser::Error), + #[error("cannot transcode more than one struct to `Event`")] + MultipleStructs, + #[error("no event produced by serialization")] + NoEvent, +} + +impl ser::Error for Error { + fn custom(msg: T) -> Self + where + T: fmt::Display, + { + Error::Custom(msg.to_string().into_boxed_str()) + } +} diff --git a/cosmwasm/packages/cw_transcode/src/lib.rs b/cosmwasm/packages/cw_transcode/src/lib.rs new file mode 100644 index 000000000..ee2f2bf47 --- /dev/null +++ b/cosmwasm/packages/cw_transcode/src/lib.rs @@ -0,0 +1,285 @@ +//! This crate provides a mechanism to convert arbitrary rust structs into `cosmwasm_std::Event`s +//! via that struct's `Serialize` impl. + +use cosmwasm_std::Event; +use serde::Serialize; + +mod error; +mod ser; + +pub use error::Error; +use ser::Serializer; + +/// Convert `value` into a `cosmwasm_std::Event` via its `Serialize` impl. +/// +/// `value` must serialize as exactly one regular struct, a unit struct, a struct variant of an +/// enum, or an `Option` that contains a value that serializes as one of of the previous 3 types. +/// Attempting to convert any other type (including `Option::None`) to an `Event` will return +/// `Error::NotAStruct`. +/// +/// The name of the struct will become the type of the returned event while the fields of the struct +/// will become the event attributes. In the case of a struct variant of an enum, the type of the +/// event will be "{name}::{variant}". Field values are encoded using the `serde-json-wasm` crate +/// and field types may be any type supported by that crate. +/// +/// # Examples +/// +/// ``` +/// # fn example() -> Result<(), cw_transcode::Error> { +/// # use cosmwasm_std::{Binary, Event}; +/// # use serde::Serialize; +/// # +/// # #[derive(Serialize)] +/// # struct Nested { +/// # f1: u32, +/// # f2: String, +/// # } +/// # +/// # #[derive(Serialize)] +/// # struct Example { +/// # primitive: i64, +/// # nested: Nested, +/// # binary: Binary, +/// # } +/// # +/// use cw_transcode::to_event; +/// +/// let e = Example { +/// // Primitive values are supported. +/// primitive: 0x9a82f2b2865c2fdau64 as i64, +/// +/// // Nested struct fields are encoded as json objects. +/// nested: Nested { +/// f1: 0xdbc7b8a1u32, +/// f2: "TEST".to_string(), +/// }, +/// +/// // Other types supported by `serde-json-wasm` are also fine. +/// binary: Binary(vec![ +/// 0xdb, 0x78, 0x97, 0x0e, 0xf2, 0x55, 0x97, 0xf4, 0x66, 0x11, 0x91, 0x0f, 0x50, 0x57, +/// 0xb8, 0xe7, +/// ]), +/// }; +/// +/// let evt = to_event(&e)?; +/// +/// let expected = Event::new("Example") +/// .add_attribute("primitive", "-7313015996323975206") +/// .add_attribute("nested", r#"{"f1":3687299233,"f2":"TEST"}"#) +/// .add_attribute("binary", "\"23iXDvJVl/RmEZEPUFe45w==\""); +/// +/// assert_eq!(expected, evt); +/// # +/// # Ok(()) +/// # } +/// # +/// # example().unwrap(); +/// ``` +pub fn to_event(value: &T) -> Result { + let mut s = Serializer::new(); + value.serialize(&mut s)?; + s.finish().ok_or(Error::NoEvent) +} + +#[cfg(test)] +mod test { + use super::*; + + use std::{any::type_name, collections::BTreeMap}; + + use cosmwasm_std::Binary; + use serde_bytes::ByteBuf; + + #[test] + fn unsupported() { + macro_rules! test_unsupported { + ($($ty:ty),* $(,)?) => { + $( + let x: $ty = Default::default(); + let name = type_name::<$ty>(); + let err = to_event(&x).expect_err(name); + assert!(matches!(err, Error::NotAStruct), "{name}"); + )* + } + } + + test_unsupported!( + i8, u8, i16, u16, i32, u32, i64, u64, i128, u128, f32, f64, char, + String, Vec, ByteBuf, BTreeMap, Option, (), (u32, f32, i64) + ); + } + + #[test] + fn basic() { + #[derive(Serialize)] + struct Transfer { + tx_hash: Binary, + timestamp: u32, + nonce: u32, + emitter_chain: u16, + #[serde(with = "hex")] + emitter_address: [u8; 32], + sequence: u64, + consistency_level: u8, + payload: Binary, + } + + let tx = Transfer { + tx_hash: Binary(vec![ + 0x82, 0xea, 0x25, 0x36, 0xc5, 0xd1, 0x67, 0x18, 0x30, 0xcb, 0x49, 0x12, 0x0f, 0x94, + 0x47, 0x9e, 0x34, 0xb5, 0x45, 0x96, 0xa8, 0xdd, 0x36, 0x9f, 0xbc, 0x26, 0x66, 0x66, + 0x7a, 0x76, 0x5f, 0x4b, + ]), + timestamp: 1672860466, + nonce: 0, + emitter_chain: 2, + emitter_address: [ + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x90, + 0xfb, 0x16, 0x72, 0x08, 0xaf, 0x45, 0x5b, 0xb1, 0x37, 0x78, 0x01, 0x63, 0xb7, 0xb7, + 0xa9, 0xa1, 0x0c, 0x16, + ], + sequence: 1672860466, + consistency_level: 15, + payload: Binary(vec![ + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0d, 0xe0, 0xb6, + 0xb3, 0xa7, 0x64, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x2d, 0x8b, 0xe6, 0xbf, 0x0b, 0xaa, 0x74, 0xe0, 0xa9, 0x07, 0x01, + 0x66, 0x79, 0xca, 0xe9, 0x19, 0x0e, 0x80, 0xdd, 0x0a, 0x00, 0x02, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xc1, 0x08, 0x20, 0x98, 0x3f, + 0x33, 0x45, 0x6c, 0xe7, 0xbe, 0xb3, 0xa0, 0x46, 0xf5, 0xa8, 0x3f, 0xa3, 0x4f, 0x02, + 0x7d, 0x0c, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + ]), + }; + + let expected = Event::new("Transfer") + .add_attribute( + "tx_hash", + "\"guolNsXRZxgwy0kSD5RHnjS1RZao3TafvCZmZnp2X0s=\"", + ) + .add_attribute("timestamp", "1672860466") + .add_attribute("nonce", "0") + .add_attribute("emitter_chain", "2") + .add_attribute( + "emitter_address", + "\"0000000000000000000000000290fb167208af455bb137780163b7b7a9a10c16\"", + ) + .add_attribute("sequence", "1672860466") + .add_attribute("consistency_level", "15") + .add_attribute("payload", "\"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA3gtrOnZAAAAAAAAAAAAAAAAAAALYvmvwuqdOCpBwFmecrpGQ6A3QoAAgAAAAAAAAAAAAAAAMEIIJg/M0Vs576zoEb1qD+jTwJ9DCAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==\""); + + let actual = to_event(&tx).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn option() { + #[derive(Serialize)] + struct Data { + a: u32, + b: String, + } + + let d = Some(Data { + a: 17, + b: "BEEF".into(), + }); + + let expected = Event::new("Data") + .add_attribute("a", "17") + .add_attribute("b", "\"BEEF\""); + + let actual = to_event(&d).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn unit_struct() { + #[derive(Serialize)] + struct MyEvent; + + let expected = Event::new("MyEvent"); + let actual = to_event(&MyEvent).unwrap(); + assert_eq!(expected, actual); + } + + #[test] + fn enum_variants() { + #[derive(Serialize)] + enum MyEvent { + A, + B(u32), + C { f1: u64, f2: String }, + D(u16, u16, u32), + } + + // Unit variants. + { + let expected = Event::new("MyEvent::A"); + let actual = to_event(&MyEvent::A).unwrap(); + assert_eq!(expected, actual); + } + + // Newtype variants. + { + let err = to_event(&MyEvent::B(19)).unwrap_err(); + assert!(matches!(err, Error::NotAStruct)); + } + + // Struct variants. + { + let c = MyEvent::C { + f1: 500, + f2: "test struct variant".into(), + }; + let expected = Event::new("MyEvent::C") + .add_attribute("f1", "500") + .add_attribute("f2", "\"test struct variant\""); + let actual = to_event(&c).unwrap(); + assert_eq!(expected, actual); + } + + // Tuple variants. + { + let err = to_event(&MyEvent::D(37, 1926, 189174)).unwrap_err(); + assert!(matches!(err, Error::NotAStruct)); + } + } + + #[test] + fn nested() { + #[derive(Serialize)] + struct Nested { + a: u32, + b: Binary, + } + + #[derive(Serialize)] + struct Outer { + nested: Nested, + c: String, + } + + let o = Outer { + nested: Nested { + a: 0xfeb42045, + b: Binary(vec![ + 0x11, 0x7d, 0x83, 0xa4, 0x06, 0xbf, 0x3e, 0x50, 0xe1, 0xaa, 0x19, 0x89, 0xc3, + 0x00, 0xea, 0xaf, + ]), + }, + c: "TEST".into(), + }; + + let expected = Event::new("Outer") + .add_attribute( + "nested", + r#"{"a":4273217605,"b":"EX2DpAa/PlDhqhmJwwDqrw=="}"#, + ) + .add_attribute("c", "\"TEST\""); + let actual = to_event(&o).unwrap(); + assert_eq!(expected, actual); + } +} diff --git a/cosmwasm/packages/cw_transcode/src/ser.rs b/cosmwasm/packages/cw_transcode/src/ser.rs new file mode 100644 index 000000000..139e44f08 --- /dev/null +++ b/cosmwasm/packages/cw_transcode/src/ser.rs @@ -0,0 +1,328 @@ +use std::fmt::Display; + +use cosmwasm_std::{Attribute, Event}; +use serde::{ + ser::{Impossible, SerializeStruct, SerializeStructVariant}, + Serialize, +}; + +use crate::Error; + +pub struct Serializer(Option); + +impl Serializer { + pub const fn new() -> Self { + Self(None) + } + + pub fn finish(self) -> Option { + self.0 + } +} + +impl<'a> serde::Serializer for &'a mut Serializer { + type Ok = (); + type Error = Error; + + type SerializeSeq = Impossible; + type SerializeTuple = Impossible; + type SerializeTupleStruct = Impossible; + type SerializeTupleVariant = Impossible; + type SerializeMap = Impossible; + type SerializeStruct = Transcoder<'a>; + type SerializeStructVariant = Transcoder<'a>; + + #[inline] + fn serialize_bool(self, _: bool) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_i8(self, _: i8) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_i16(self, _: i16) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_i32(self, _: i32) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_i64(self, _: i64) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_i128(self, _: i128) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_u8(self, _: u8) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_u16(self, _: u16) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_u32(self, _: u32) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_u64(self, _: u64) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_u128(self, _: u128) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_f32(self, _: f32) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_f64(self, _: f64) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_char(self, _: char) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_str(self, _: &str) -> Result { + Err(Error::NotAStruct) + } + + fn serialize_bytes(self, _: &[u8]) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_none(self) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_some(self, value: &T) -> Result + where + T: Serialize, + { + value.serialize(self) + } + + #[inline] + fn serialize_unit(self) -> Result { + Err(Error::NotAStruct) + } + + fn serialize_unit_struct(self, name: &'static str) -> Result { + if self.0.is_none() { + self.0 = Some(Event::new(name)); + Ok(()) + } else { + Err(Error::MultipleStructs) + } + } + + fn serialize_unit_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + ) -> Result { + if self.0.is_none() { + self.0 = Some(Event::new(format!("{name}::{variant}"))); + Ok(()) + } else { + Err(Error::MultipleStructs) + } + } + + #[inline] + fn serialize_newtype_struct( + self, + _: &'static str, + _: &T, + ) -> Result + where + T: Serialize, + { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_newtype_variant( + self, + _: &'static str, + _variant_index: u32, + _: &'static str, + _: &T, + ) -> Result + where + T: Serialize, + { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_tuple_variant( + self, + _: &'static str, + _variant_index: u32, + _: &'static str, + _: usize, + ) -> Result { + Err(Error::NotAStruct) + } + + fn serialize_struct_variant( + self, + name: &'static str, + _variant_index: u32, + variant: &'static str, + _: usize, + ) -> Result { + if self.0.is_none() { + Ok(Transcoder { + serializer: self, + event: Event::new(format!("{name}::{variant}")), + }) + } else { + Err(Error::MultipleStructs) + } + } + + #[inline] + fn serialize_seq(self, _: Option) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_tuple(self, _: usize) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_tuple_struct( + self, + _: &'static str, + _: usize, + ) -> Result { + Err(Error::NotAStruct) + } + + #[inline] + fn serialize_map(self, _: Option) -> Result { + Err(Error::NotAStruct) + } + + fn serialize_struct( + self, + name: &'static str, + _: usize, + ) -> Result { + if self.0.is_none() { + Ok(Transcoder { + serializer: self, + event: Event::new(name), + }) + } else { + Err(Error::MultipleStructs) + } + } + + #[inline] + fn collect_str(self, _: &T) -> Result + where + T: Display, + { + Err(Error::NotAStruct) + } + + #[inline] + fn is_human_readable(&self) -> bool { + true + } +} + +pub struct Transcoder<'a> { + serializer: &'a mut Serializer, + event: Event, +} + +impl<'a> Transcoder<'a> { + fn serialize_field(&mut self, k: &'static str, v: &T) -> Result<(), Error> + where + T: Serialize, + { + let key = k.into(); + let value = serde_json_wasm::to_string(v)?; + self.event.attributes.push(Attribute { key, value }); + + Ok(()) + } + + #[inline] + fn end(self) -> Result<(), Error> { + self.serializer.0 = Some(self.event); + Ok(()) + } +} + +impl<'a> SerializeStruct for Transcoder<'a> { + type Ok = (); + type Error = Error; + + #[inline] + fn serialize_field( + &mut self, + k: &'static str, + v: &T, + ) -> Result + where + T: Serialize, + { + self.serialize_field(k, v) + } + + #[inline] + fn end(self) -> Result { + self.end() + } +} + +impl<'a> SerializeStructVariant for Transcoder<'a> { + type Ok = (); + type Error = Error; + + #[inline] + fn serialize_field( + &mut self, + k: &'static str, + v: &T, + ) -> Result + where + T: Serialize, + { + self.serialize_field(k, v) + } + + #[inline] + fn end(self) -> Result { + self.end() + } +}