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.
This commit is contained in:
Chirantan Ekbote 2023-01-06 17:49:45 +09:00 committed by Chirantan Ekbote
parent ce1f3adc23
commit 3c914c725a
6 changed files with 671 additions and 0 deletions

15
cosmwasm/Cargo.lock generated
View File

@ -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"

View File

@ -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" }

View File

@ -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"

View File

@ -0,0 +1,26 @@
use std::fmt;
use serde::ser;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Custom(Box<str>),
#[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<T>(msg: T) -> Self
where
T: fmt::Display,
{
Error::Custom(msg.to_string().into_boxed_str())
}
}

View File

@ -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<T: Serialize + ?Sized>(value: &T) -> Result<Event, Error> {
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<u8>, ByteBuf, BTreeMap<u32, u32>, Option<u64>, (), (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);
}
}

View File

@ -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<Event>);
impl Serializer {
pub const fn new() -> Self {
Self(None)
}
pub fn finish(self) -> Option<Event> {
self.0
}
}
impl<'a> serde::Serializer for &'a mut Serializer {
type Ok = ();
type Error = Error;
type SerializeSeq = Impossible<Self::Ok, Self::Error>;
type SerializeTuple = Impossible<Self::Ok, Self::Error>;
type SerializeTupleStruct = Impossible<Self::Ok, Self::Error>;
type SerializeTupleVariant = Impossible<Self::Ok, Self::Error>;
type SerializeMap = Impossible<Self::Ok, Self::Error>;
type SerializeStruct = Transcoder<'a>;
type SerializeStructVariant = Transcoder<'a>;
#[inline]
fn serialize_bool(self, _: bool) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_i8(self, _: i8) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_i16(self, _: i16) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_i32(self, _: i32) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_i64(self, _: i64) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_i128(self, _: i128) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_u8(self, _: u8) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_u16(self, _: u16) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_u32(self, _: u32) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_u64(self, _: u64) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_u128(self, _: u128) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_f32(self, _: f32) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_f64(self, _: f64) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_char(self, _: char) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_str(self, _: &str) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
fn serialize_bytes(self, _: &[u8]) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_some<T: ?Sized>(self, value: &T) -> Result<Self::Ok, Self::Error>
where
T: Serialize,
{
value.serialize(self)
}
#[inline]
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
Err(Error::NotAStruct)
}
fn serialize_unit_struct(self, name: &'static str) -> Result<Self::Ok, Self::Error> {
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<Self::Ok, Self::Error> {
if self.0.is_none() {
self.0 = Some(Event::new(format!("{name}::{variant}")));
Ok(())
} else {
Err(Error::MultipleStructs)
}
}
#[inline]
fn serialize_newtype_struct<T: ?Sized>(
self,
_: &'static str,
_: &T,
) -> Result<Self::Ok, Self::Error>
where
T: Serialize,
{
Err(Error::NotAStruct)
}
#[inline]
fn serialize_newtype_variant<T: ?Sized>(
self,
_: &'static str,
_variant_index: u32,
_: &'static str,
_: &T,
) -> Result<Self::Ok, Self::Error>
where
T: Serialize,
{
Err(Error::NotAStruct)
}
#[inline]
fn serialize_tuple_variant(
self,
_: &'static str,
_variant_index: u32,
_: &'static str,
_: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
Err(Error::NotAStruct)
}
fn serialize_struct_variant(
self,
name: &'static str,
_variant_index: u32,
variant: &'static str,
_: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
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<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_tuple(self, _: usize) -> Result<Self::SerializeTuple, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_tuple_struct(
self,
_: &'static str,
_: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
Err(Error::NotAStruct)
}
#[inline]
fn serialize_map(self, _: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Err(Error::NotAStruct)
}
fn serialize_struct(
self,
name: &'static str,
_: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
if self.0.is_none() {
Ok(Transcoder {
serializer: self,
event: Event::new(name),
})
} else {
Err(Error::MultipleStructs)
}
}
#[inline]
fn collect_str<T: ?Sized>(self, _: &T) -> Result<Self::Ok, Self::Error>
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<T: ?Sized>(&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<T: ?Sized>(
&mut self,
k: &'static str,
v: &T,
) -> Result<Self::Ok, Self::Error>
where
T: Serialize,
{
self.serialize_field(k, v)
}
#[inline]
fn end(self) -> Result<Self::Ok, Self::Error> {
self.end()
}
}
impl<'a> SerializeStructVariant for Transcoder<'a> {
type Ok = ();
type Error = Error;
#[inline]
fn serialize_field<T: ?Sized>(
&mut self,
k: &'static str,
v: &T,
) -> Result<Self::Ok, Self::Error>
where
T: Serialize,
{
self.serialize_field(k, v)
}
#[inline]
fn end(self) -> Result<Self::Ok, Self::Error> {
self.end()
}
}