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() + } +}