From c580365654990b778dbcda4042045a46368f8074 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wo=C5=BAniak?= Date: Thu, 17 Oct 2024 18:15:24 +0200 Subject: [PATCH 1/3] feat: Add auto deserialization of reply data (#445) --- .../src/contract/communication/reply.rs | 75 ++++- sylvia-derive/src/parser/attributes/data.rs | 48 ++++ sylvia-derive/src/parser/attributes/mod.rs | 10 + sylvia-derive/src/types/msg_field.rs | 4 + sylvia/tests/reply_data.rs | 270 ++++++++++++++++++ sylvia/tests/{reply.rs => reply_dispatch.rs} | 4 +- sylvia/tests/reply_generation.rs | 2 +- .../ui/attributes/data/missing_attribute.rs | 33 +++ .../attributes/data/missing_attribute.stderr | 9 + sylvia/tests/ui/method_signature/reply.rs | 8 +- 10 files changed, 455 insertions(+), 8 deletions(-) create mode 100644 sylvia-derive/src/parser/attributes/data.rs create mode 100644 sylvia/tests/reply_data.rs rename sylvia/tests/{reply.rs => reply_dispatch.rs} (99%) create mode 100644 sylvia/tests/ui/attributes/data/missing_attribute.rs create mode 100644 sylvia/tests/ui/attributes/data/missing_attribute.stderr diff --git a/sylvia-derive/src/contract/communication/reply.rs b/sylvia-derive/src/contract/communication/reply.rs index 0a528200..b2e8b1a0 100644 --- a/sylvia-derive/src/contract/communication/reply.rs +++ b/sylvia-derive/src/contract/communication/reply.rs @@ -6,7 +6,7 @@ use syn::{parse_quote, GenericParam, Ident, ItemImpl, Type}; use crate::crate_module; use crate::parser::attributes::msg::ReplyOn; -use crate::parser::{MsgType, SylviaAttribute}; +use crate::parser::{MsgType, ParsedSylviaAttributes, SylviaAttribute}; use crate::types::msg_field::MsgField; use crate::types::msg_variant::{MsgVariant, MsgVariants}; use crate::utils::emit_turbofish; @@ -190,12 +190,15 @@ struct ReplyData<'a> { pub handler_id: &'a Ident, /// Methods handling the reply id for the associated reply on. pub handlers: Vec<(&'a Ident, ReplyOn)>, + /// Data parameter associated with the handlers. + pub data: Option<&'a MsgField<'a>>, /// Payload parameters associated with the handlers. pub payload: Vec<&'a MsgField<'a>>, } impl<'a> ReplyData<'a> { pub fn new(reply_id: Ident, variant: &'a MsgVariant<'a>, handler_id: &'a Ident) -> Self { + let data = variant.fields().first(); // Skip the first field reserved for the `data`. let payload = variant.fields().iter().skip(1).collect::>(); let method_name = variant.function_name(); @@ -205,6 +208,7 @@ impl<'a> ReplyData<'a> { reply_id, handler_id, handlers: vec![(method_name, reply_on)], + data, payload, } } @@ -372,12 +376,14 @@ impl<'a> ReplyData<'a> { Some((method_name, reply_on)) if reply_on == &ReplyOn::Success => { let payload_values = self.payload.iter().map(|field| field.name()); let payload_deserialization = self.payload.emit_payload_deserialization(); + let data_deserialization = self.data.map(DataField::emit_data_deserialization); quote! { #sylvia ::cw_std::SubMsgResult::Ok(sub_msg_resp) => { #[allow(deprecated)] let #sylvia ::cw_std::SubMsgResponse { events, data, msg_responses} = sub_msg_resp; #payload_deserialization + #data_deserialization #contract_turbofish ::new(). #method_name ((deps, env, gas_used, events, msg_responses).into(), data, #(#payload_values),* ) } @@ -475,6 +481,73 @@ impl<'a> ReplyVariant<'a> for MsgVariant<'a> { } } +pub trait DataField { + fn emit_data_deserialization(&self) -> TokenStream; +} + +impl DataField for MsgField<'_> { + fn emit_data_deserialization(&self) -> TokenStream { + let sylvia = crate_module(); + let data = ParsedSylviaAttributes::new(self.attrs().iter()).data; + let is_data_attr = self + .attrs() + .iter() + .any(|attr| SylviaAttribute::new(attr) == Some(SylviaAttribute::Data)); + let missing_data_err = "Missing reply data field."; + let invalid_reply_data_err = quote! { + format! {"Invalid reply data: {}\nSerde error while deserializing {}", data, err} + }; + let data_deserialization = quote! { + let deserialized_data = + #sylvia ::cw_utils::parse_execute_response_data(data.as_slice()) + .map_err(|err| #sylvia ::cw_std::StdError::generic_err( + format!("Failed deserializing protobuf data: {}", err) + ))?; + let deserialized_data = match deserialized_data.data { + Some(data) => #sylvia ::cw_std::from_json(&data).map_err(|err| #sylvia ::cw_std::StdError::generic_err( #invalid_reply_data_err ))?, + None => return Err(Into::into( #sylvia ::cw_std::StdError::generic_err( #missing_data_err ))), + }; + }; + + match data { + Some(data) if data.raw && data.opt => quote! {}, + Some(data) if data.raw => quote! { + let data = match data { + Some(data) => data, + None => return Err(Into::into( #sylvia ::cw_std::StdError::generic_err( #missing_data_err ))), + }; + }, + Some(data) if data.opt => quote! { + let data = match data { + Some(data) => { + #data_deserialization + + Some(deserialized_data) + }, + None => None, + }; + }, + None if is_data_attr => quote! { + let data = match data { + Some(data) => { + #data_deserialization + + deserialized_data + }, + None => return Err(Into::into( #sylvia ::cw_std::StdError::generic_err( #missing_data_err ))), + }; + }, + _ => { + emit_error!(self.name().span(), "Invalid data usage."; + note = "Reply data should be marked with #[sv::data] attribute."; + note = "Remove this parameter or mark it with #[sv::data] attribute." + ); + quote! {} + } + } + } +} + pub trait PayloadFields { fn emit_payload_deserialization(&self) -> TokenStream; fn emit_payload_serialization(&self) -> TokenStream; diff --git a/sylvia-derive/src/parser/attributes/data.rs b/sylvia-derive/src/parser/attributes/data.rs new file mode 100644 index 00000000..b88fa8bb --- /dev/null +++ b/sylvia-derive/src/parser/attributes/data.rs @@ -0,0 +1,48 @@ +use proc_macro_error::emit_error; +use syn::parse::{Parse, ParseStream, Parser}; +use syn::{Error, Ident, MetaList, Result, Token}; + +/// Type wrapping data parsed from `sv::data` attribute. +#[derive(Default, Debug)] +pub struct DataFieldParams { + pub raw: bool, + pub opt: bool, +} + +impl DataFieldParams { + pub fn new(attr: &MetaList) -> Result { + DataFieldParams::parse + .parse2(attr.tokens.clone()) + .map_err(|err| { + emit_error!(err.span(), err); + err + }) + } +} + +impl Parse for DataFieldParams { + fn parse(input: ParseStream) -> Result { + let mut data = Self::default(); + + while !input.is_empty() { + let option: Ident = input.parse()?; + match option.to_string().as_str() { + "raw" => data.raw = true, + "opt" => data.opt = true, + _ => { + return Err(Error::new( + option.span(), + "Invalid data parameter.\n + = note: Expected one of [`raw`, `opt`] comma separated.\n", + )) + } + } + if !input.peek(Token![,]) { + break; + } + let _: Token![,] = input.parse()?; + } + + Ok(data) + } +} diff --git a/sylvia-derive/src/parser/attributes/mod.rs b/sylvia-derive/src/parser/attributes/mod.rs index 2134e818..215bda73 100644 --- a/sylvia-derive/src/parser/attributes/mod.rs +++ b/sylvia-derive/src/parser/attributes/mod.rs @@ -1,6 +1,7 @@ //! Module defining parsing of Sylvia attributes. //! Every Sylvia attribute should be prefixed with `sv::` +use data::DataFieldParams; use features::SylviaFeatures; use proc_macro_error::emit_error; use syn::spanned::Spanned; @@ -8,6 +9,7 @@ use syn::{Attribute, MetaList, PathSegment}; pub mod attr; pub mod custom; +pub mod data; pub mod error; pub mod features; pub mod messages; @@ -33,6 +35,7 @@ pub enum SylviaAttribute { VariantAttrs, MsgAttrs, Payload, + Data, Features, } @@ -56,6 +59,7 @@ impl SylviaAttribute { "attr" => Some(Self::VariantAttrs), "msg_attr" => Some(Self::MsgAttrs), "payload" => Some(Self::Payload), + "data" => Some(Self::Data), "features" => Some(Self::Features), _ => None, } @@ -74,6 +78,7 @@ pub struct ParsedSylviaAttributes { pub variant_attrs_forward: Vec, pub msg_attrs_forward: Vec, pub sv_features: SylviaFeatures, + pub data: Option, } impl ParsedSylviaAttributes { @@ -172,6 +177,11 @@ impl ParsedSylviaAttributes { note = attr.span() => "The `sv::payload` should be used as a prefix for `Binary` payload."; ); } + SylviaAttribute::Data => { + if let Ok(data) = DataFieldParams::new(attr) { + self.data = Some(data); + } + } SylviaAttribute::Features => { if let Ok(features) = SylviaFeatures::new(attr) { self.sv_features = features; diff --git a/sylvia-derive/src/types/msg_field.rs b/sylvia-derive/src/types/msg_field.rs index 2834ed71..46bfffc4 100644 --- a/sylvia-derive/src/types/msg_field.rs +++ b/sylvia-derive/src/types/msg_field.rs @@ -121,6 +121,10 @@ impl<'a> MsgField<'a> { self.ty } + pub fn attrs(&self) -> &'a Vec { + self.attrs + } + pub fn contains_attribute(&self, sv_attr: SylviaAttribute) -> bool { self.attrs .iter() diff --git a/sylvia/tests/reply_data.rs b/sylvia/tests/reply_data.rs new file mode 100644 index 00000000..3a493844 --- /dev/null +++ b/sylvia/tests/reply_data.rs @@ -0,0 +1,270 @@ +use cosmwasm_schema::cw_serde; +use cosmwasm_std::to_json_binary; +use cw_storage_plus::Item; +use cw_utils::{parse_instantiate_response_data, ParseReplyError}; +use noop_contract::sv::{Executor, NoopContractInstantiateBuilder}; +use sv::SubMsgMethods; +use sylvia::builder::instantiate::InstantiateBuilder; +use sylvia::cw_std::{Addr, Binary, Response, StdError, SubMsg}; +use sylvia::types::{ExecCtx, InstantiateCtx, Remote, ReplyCtx}; +use sylvia::{contract, entry_points}; +use thiserror::Error; + +#[allow(dead_code)] +mod noop_contract { + use cosmwasm_std::{Binary, StdResult}; + use sylvia::types::{ExecCtx, InstantiateCtx}; + use sylvia::{contract, entry_points}; + + use sylvia::cw_std::Response; + + pub struct NoopContract; + + #[entry_points] + #[contract] + impl NoopContract { + pub const fn new() -> Self { + Self + } + + #[sv::msg(instantiate)] + fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult { + Ok(Response::new()) + } + + #[sv::msg(exec)] + fn noop(&self, _ctx: ExecCtx, data: Option) -> StdResult { + let resp = match data { + Some(data) => Response::new().set_data(data), + None => Response::new(), + }; + + Ok(resp) + } + } +} + +#[cw_serde] +pub struct InstantiatePayload { + pub sender: Addr, +} + +#[derive(Error, Debug, PartialEq)] +pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("{0}")] + ParseReply(#[from] ParseReplyError), +} + +pub struct Contract { + remote: Item>, +} + +#[entry_points] +#[contract] +#[sv::error(ContractError)] +#[sv::features(replies)] +impl Contract { + pub fn new() -> Self { + Self { + remote: Item::new("remote"), + } + } + + #[sv::msg(instantiate)] + pub fn instantiate( + &self, + ctx: InstantiateCtx, + remote_code_id: u64, + ) -> Result { + // Custom type can be used as a payload. + let payload = InstantiatePayload { + sender: ctx.info.sender, + }; + let sub_msg = InstantiateBuilder::noop_contract(remote_code_id)? + .with_label("noop") + .build() + .remote_instantiated(to_json_binary(&payload)?)?; + // TODO: Blocked by https://github.com/CosmWasm/cw-multi-test/pull/216. Uncomment when new + // MultiTest version is released. + // Payload is not currently forwarded in the MultiTest. + // .remote_instantiated(payload)?; + + Ok(Response::new().add_submessage(sub_msg)) + } + + #[sv::msg(exec)] + fn send_message_expecting_data( + &self, + ctx: ExecCtx, + data: Option, + reply_id: u64, + ) -> Result { + let msg = self + .remote + .load(ctx.deps.storage)? + .executor() + .noop(data)? + .build(); + let submsg = SubMsg::reply_on_success(msg, reply_id); + + Ok(Response::new().add_submessage(submsg)) + } + + #[sv::msg(reply, reply_on=success)] + fn remote_instantiated( + &self, + ctx: ReplyCtx, + #[sv::data(raw, opt)] data: Option, + // TODO: Blocked by https://github.com/CosmWasm/cw-multi-test/pull/216. Uncomment when new + // MultiTest version is released. + // Payload is not currently forwarded in the MultiTest. + // _instantiate_payload: InstantiatePayload, + #[sv::payload] _payload: Binary, + ) -> Result { + let init_data = parse_instantiate_response_data(&data.unwrap())?; + let remote_addr = Addr::unchecked(init_data.contract_address); + + self.remote + .save(ctx.deps.storage, &Remote::new(remote_addr))?; + + Ok(Response::new()) + } + + #[sv::msg(reply, reply_on=success)] + fn data_raw_opt( + &self, + _ctx: ReplyCtx, + #[sv::data(raw, opt)] _data: Option, + #[sv::payload] _payload: Binary, + ) -> Result { + Ok(Response::new()) + } + + #[sv::msg(reply, reply_on=success)] + fn data_raw( + &self, + _ctx: ReplyCtx, + #[sv::data(raw)] _data: Binary, + #[sv::payload] _payload: Binary, + ) -> Result { + Ok(Response::new()) + } + + #[sv::msg(reply, reply_on=success)] + fn data_opt( + &self, + _ctx: ReplyCtx, + #[sv::data(opt)] _data: Option, + #[sv::payload] _payload: Binary, + ) -> Result { + Ok(Response::new()) + } + + #[sv::msg(reply, reply_on=success)] + fn data( + &self, + _ctx: ReplyCtx, + #[sv::data] _data: String, + #[sv::payload] _payload: Binary, + ) -> Result { + Ok(Response::new()) + } +} + +mod tests { + use crate::noop_contract::sv::mt::CodeId as NoopCodeId; + use crate::sv::mt::{CodeId, ContractProxy}; + use crate::sv::{DATA_OPT_REPLY_ID, DATA_RAW_OPT_REPLY_ID, DATA_RAW_REPLY_ID, DATA_REPLY_ID}; + + use cosmwasm_std::{to_json_binary, Binary, StdError}; + use sylvia::cw_multi_test::IntoBech32; + use sylvia::multitest::App; + + #[test] + fn dispatch_replies() { + let app = App::default(); + let code_id = CodeId::store_code(&app); + let noop_code_id = NoopCodeId::store_code(&app); + + let owner = "owner".into_bech32(); + let data = Some(to_json_binary(&String::from("some_data")).unwrap()); + let invalid_data = Some(Binary::from("InvalidData".as_bytes())); + + // Trigger remote instantiation reply + let contract = code_id + .instantiate(noop_code_id.code_id()) + .with_label("Contract") + .call(&owner) + .unwrap(); + + // Should forward `data` in every case + contract + .send_message_expecting_data(None, DATA_RAW_OPT_REPLY_ID) + .call(&owner) + .unwrap(); + + contract + .send_message_expecting_data(data.clone(), DATA_RAW_OPT_REPLY_ID) + .call(&owner) + .unwrap(); + + // Should forward `data` if `Some` and return error if `None` + let err = contract + .send_message_expecting_data(None, DATA_RAW_REPLY_ID) + .call(&owner) + .unwrap_err(); + assert_eq!( + err, + StdError::generic_err("Missing reply data field.").into() + ); + + contract + .send_message_expecting_data(data.clone(), DATA_RAW_REPLY_ID) + .call(&owner) + .unwrap(); + + // Should forward deserialized `data` if `Some` or None and return error if deserialization fails + contract + .send_message_expecting_data(None, DATA_OPT_REPLY_ID) + .call(&owner) + .unwrap(); + + let err = contract + .send_message_expecting_data(invalid_data.clone(), DATA_OPT_REPLY_ID) + .call(&owner) + .unwrap_err(); + assert_eq!( + err, + StdError::generic_err("Invalid reply data: SW52YWxpZERhdGE=\nSerde error while deserializing Error parsing into type alloc::string::String: Invalid type").into() + ); + + contract + .send_message_expecting_data(data.clone(), DATA_OPT_REPLY_ID) + .call(&owner) + .unwrap(); + + // Should forward deserialized `data` if `Some` and return error if `None` or if deserialization fails + let err = contract + .send_message_expecting_data(None, DATA_REPLY_ID) + .call(&owner) + .unwrap_err(); + assert_eq!( + err, + StdError::generic_err("Missing reply data field.").into() + ); + + let err = contract + .send_message_expecting_data(invalid_data, DATA_REPLY_ID) + .call(&owner) + .unwrap_err(); + assert_eq!(err, StdError::generic_err("Invalid reply data: SW52YWxpZERhdGE=\nSerde error while deserializing Error parsing into type alloc::string::String: Invalid type").into()); + + contract + .send_message_expecting_data(data, DATA_REPLY_ID) + .call(&owner) + .unwrap(); + } +} diff --git a/sylvia/tests/reply.rs b/sylvia/tests/reply_dispatch.rs similarity index 99% rename from sylvia/tests/reply.rs rename to sylvia/tests/reply_dispatch.rs index ed478151..23569c5c 100644 --- a/sylvia/tests/reply.rs +++ b/sylvia/tests/reply_dispatch.rs @@ -214,7 +214,7 @@ where fn remote_instantiated( &self, ctx: ReplyCtx, - data: Option, + #[sv::data(raw, opt)] data: Option, // TODO: Blocked by https://github.com/CosmWasm/cw-multi-test/pull/216. Uncomment when new // MultiTest version is released. // Payload is not currently forwarded in the MultiTest. @@ -236,7 +236,7 @@ where fn success( &self, ctx: ReplyCtx, - _data: Option, + #[sv::data(raw, opt)] _data: Option, #[sv::payload] _payload: Binary, ) -> Result, ContractError> { self.last_reply.save(ctx.deps.storage, &SUCCESS_REPLY_ID)?; diff --git a/sylvia/tests/reply_generation.rs b/sylvia/tests/reply_generation.rs index a4f471ca..afd65ad2 100644 --- a/sylvia/tests/reply_generation.rs +++ b/sylvia/tests/reply_generation.rs @@ -45,7 +45,7 @@ impl Contract { fn reply_on( &self, _ctx: ReplyCtx, - _data: Option, + #[sv::data(raw, opt)] _data: Option, #[sv::payload] _payload: Binary, ) -> StdResult { Ok(Response::new()) diff --git a/sylvia/tests/ui/attributes/data/missing_attribute.rs b/sylvia/tests/ui/attributes/data/missing_attribute.rs new file mode 100644 index 00000000..bed0fd3d --- /dev/null +++ b/sylvia/tests/ui/attributes/data/missing_attribute.rs @@ -0,0 +1,33 @@ +#![allow(unused_imports)] + +use sylvia::contract; +use sylvia::cw_std::{Reply, Response, StdResult}; +use sylvia::types::{InstantiateCtx, ReplyCtx}; + +pub struct Contract; + +#[contract] +#[sv::features(replies)] +impl Contract { + pub fn new() -> Self { + Self + } + + #[sv::msg(instantiate)] + pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult { + Ok(Response::new()) + } + + #[sv::msg(reply, reply_on=success)] + fn reply( + &self, + _ctx: ReplyCtx, + // If the `data` attribute is missing, the data field should be omitted. + _data: Option, + param: String, + ) -> StdResult { + Ok(Response::new()) + } +} + +fn main() {} diff --git a/sylvia/tests/ui/attributes/data/missing_attribute.stderr b/sylvia/tests/ui/attributes/data/missing_attribute.stderr new file mode 100644 index 00000000..64010df5 --- /dev/null +++ b/sylvia/tests/ui/attributes/data/missing_attribute.stderr @@ -0,0 +1,9 @@ +error: Invalid data usage. + + = note: Reply data should be marked with #[sv::data] attribute. + = note: Remove this parameter or mark it with #[sv::data] attribute. + + --> tests/ui/attributes/data/missing_attribute.rs:26:9 + | +26 | _data: Option, + | ^^^^^ diff --git a/sylvia/tests/ui/method_signature/reply.rs b/sylvia/tests/ui/method_signature/reply.rs index b6775cea..4cd6ea88 100644 --- a/sylvia/tests/ui/method_signature/reply.rs +++ b/sylvia/tests/ui/method_signature/reply.rs @@ -23,7 +23,7 @@ pub mod mismatched_params { fn first_reply( &self, _ctx: ReplyCtx, - _data: Option, + #[sv::data(opt, raw)] _data: Option, param: String, ) -> StdResult { Ok(Response::new()) @@ -33,7 +33,7 @@ pub mod mismatched_params { fn second_reply( &self, _ctx: ReplyCtx, - _data: Option, + #[sv::data(opt, raw)] _data: Option, param: u32, ) -> StdResult { Ok(Response::new()) @@ -62,7 +62,7 @@ pub mod mismatched_param_arity { fn first_reply( &self, _ctx: ReplyCtx, - _data: Option, + #[sv::data(opt, raw)] _data: Option, param: String, ) -> StdResult { Ok(Response::new()) @@ -72,7 +72,7 @@ pub mod mismatched_param_arity { fn second_reply( &self, _ctx: ReplyCtx, - _data: Option, + #[sv::data(opt, raw)] _data: Option, param: String, param: u32, ) -> StdResult { From fb2849317326db4cd67726e1eec609fb47f6eab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wo=C5=BAniak?= Date: Mon, 21 Oct 2024 16:31:26 +0200 Subject: [PATCH 2/3] feat: Add `instantiate` parameter to the `sv::data` attribute --- .../src/contract/communication/reply.rs | 34 +++++++++++++++++-- sylvia-derive/src/parser/attributes/data.rs | 19 +++++++++-- sylvia/tests/reply_data.rs | 17 +++++++--- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/sylvia-derive/src/contract/communication/reply.rs b/sylvia-derive/src/contract/communication/reply.rs index b2e8b1a0..5864b039 100644 --- a/sylvia-derive/src/contract/communication/reply.rs +++ b/sylvia-derive/src/contract/communication/reply.rs @@ -497,7 +497,7 @@ impl DataField for MsgField<'_> { let invalid_reply_data_err = quote! { format! {"Invalid reply data: {}\nSerde error while deserializing {}", data, err} }; - let data_deserialization = quote! { + let execute_data_deserialization = quote! { let deserialized_data = #sylvia ::cw_utils::parse_execute_response_data(data.as_slice()) .map_err(|err| #sylvia ::cw_std::StdError::generic_err( @@ -509,6 +509,14 @@ impl DataField for MsgField<'_> { }; }; + let instantiate_data_deserialization = quote! { + let deserialized_data = + #sylvia ::cw_utils::parse_instantiate_response_data(data.as_slice()) + .map_err(|err| #sylvia ::cw_std::StdError::generic_err( + format!("Failed deserializing protobuf data: {}", err) + ))?; + }; + match data { Some(data) if data.raw && data.opt => quote! {}, Some(data) if data.raw => quote! { @@ -517,10 +525,30 @@ impl DataField for MsgField<'_> { None => return Err(Into::into( #sylvia ::cw_std::StdError::generic_err( #missing_data_err ))), }; }, + Some(data) if data.instantiate && data.opt => quote! { + let data = match data { + Some(data) => { + #instantiate_data_deserialization + + Some(deserialized_data) + }, + None => None, + }; + }, + Some(data) if data.instantiate => quote! { + let data = match data { + Some(data) => { + #instantiate_data_deserialization + + deserialized_data + }, + None => return Err(Into::into( #sylvia ::cw_std::StdError::generic_err( #missing_data_err ))), + }; + }, Some(data) if data.opt => quote! { let data = match data { Some(data) => { - #data_deserialization + #execute_data_deserialization Some(deserialized_data) }, @@ -530,7 +558,7 @@ impl DataField for MsgField<'_> { None if is_data_attr => quote! { let data = match data { Some(data) => { - #data_deserialization + #execute_data_deserialization deserialized_data }, diff --git a/sylvia-derive/src/parser/attributes/data.rs b/sylvia-derive/src/parser/attributes/data.rs index b88fa8bb..13aefe83 100644 --- a/sylvia-derive/src/parser/attributes/data.rs +++ b/sylvia-derive/src/parser/attributes/data.rs @@ -1,5 +1,6 @@ use proc_macro_error::emit_error; use syn::parse::{Parse, ParseStream, Parser}; +use syn::spanned::Spanned; use syn::{Error, Ident, MetaList, Result, Token}; /// Type wrapping data parsed from `sv::data` attribute. @@ -7,16 +8,27 @@ use syn::{Error, Ident, MetaList, Result, Token}; pub struct DataFieldParams { pub raw: bool, pub opt: bool, + pub instantiate: bool, } impl DataFieldParams { pub fn new(attr: &MetaList) -> Result { - DataFieldParams::parse + let data = DataFieldParams::parse .parse2(attr.tokens.clone()) .map_err(|err| { emit_error!(err.span(), err); err - }) + })?; + + if data.instantiate && data.raw { + emit_error!( + attr.tokens.span(), + "The `instantiate` cannot be used in pair with `raw` parameter."; + note = "Use any combination of [`raw`, `opt`] or [`instantiate`, `opt`] pairs." + ); + } + + Ok(data) } } @@ -29,11 +41,12 @@ impl Parse for DataFieldParams { match option.to_string().as_str() { "raw" => data.raw = true, "opt" => data.opt = true, + "instantiate" => data.instantiate = true, _ => { return Err(Error::new( option.span(), "Invalid data parameter.\n - = note: Expected one of [`raw`, `opt`] comma separated.\n", + = note: Expected one of [`raw`, `opt`, `instantiate`] comma separated.\n", )) } } diff --git a/sylvia/tests/reply_data.rs b/sylvia/tests/reply_data.rs index 3a493844..f336720c 100644 --- a/sylvia/tests/reply_data.rs +++ b/sylvia/tests/reply_data.rs @@ -1,7 +1,7 @@ use cosmwasm_schema::cw_serde; use cosmwasm_std::to_json_binary; use cw_storage_plus::Item; -use cw_utils::{parse_instantiate_response_data, ParseReplyError}; +use cw_utils::{MsgInstantiateContractResponse, ParseReplyError}; use noop_contract::sv::{Executor, NoopContractInstantiateBuilder}; use sv::SubMsgMethods; use sylvia::builder::instantiate::InstantiateBuilder; @@ -117,15 +117,14 @@ impl Contract { fn remote_instantiated( &self, ctx: ReplyCtx, - #[sv::data(raw, opt)] data: Option, + #[sv::data(instantiate)] data: MsgInstantiateContractResponse, // TODO: Blocked by https://github.com/CosmWasm/cw-multi-test/pull/216. Uncomment when new // MultiTest version is released. // Payload is not currently forwarded in the MultiTest. // _instantiate_payload: InstantiatePayload, #[sv::payload] _payload: Binary, ) -> Result { - let init_data = parse_instantiate_response_data(&data.unwrap())?; - let remote_addr = Addr::unchecked(init_data.contract_address); + let remote_addr = Addr::unchecked(data.contract_address); self.remote .save(ctx.deps.storage, &Remote::new(remote_addr))?; @@ -133,6 +132,16 @@ impl Contract { Ok(Response::new()) } + #[sv::msg(reply, reply_on=success)] + fn _optional_remote_instantiated( + &self, + _ctx: ReplyCtx, + #[sv::data(instantiate, opt)] _data: Option, + #[sv::payload] _payload: Binary, + ) -> Result { + Ok(Response::new()) + } + #[sv::msg(reply, reply_on=success)] fn data_raw_opt( &self, From 30da5ec321abd1a5eeb47e77e734b2f360921337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Wo=C5=BAniak?= Date: Wed, 30 Oct 2024 20:42:26 +0100 Subject: [PATCH 3/3] feat: Support omitting data parameter Expect `(raw)` parameter in the `sv::payload` attribute. --- .../src/contract/communication/reply.rs | 72 ++++--- sylvia-derive/src/parser/attributes/mod.rs | 18 +- .../src/parser/attributes/payload.rs | 44 +++++ sylvia-derive/src/types/msg_field.rs | 7 - sylvia/tests/reply_data.rs | 181 +++++++++--------- sylvia/tests/reply_dispatch.rs | 8 +- sylvia/tests/reply_generation.rs | 18 +- ...missing_attribute.rs => invalid_params.rs} | 7 +- .../ui/attributes/data/invalid_params.stderr | 7 + .../attributes/data/missing_attribute.stderr | 9 - .../ui/attributes/payload/invalid_params.rs | 32 ++++ .../attributes/payload/invalid_params.stderr | 6 + 12 files changed, 262 insertions(+), 147 deletions(-) create mode 100644 sylvia-derive/src/parser/attributes/payload.rs rename sylvia/tests/ui/attributes/data/{missing_attribute.rs => invalid_params.rs} (74%) create mode 100644 sylvia/tests/ui/attributes/data/invalid_params.stderr delete mode 100644 sylvia/tests/ui/attributes/data/missing_attribute.stderr create mode 100644 sylvia/tests/ui/attributes/payload/invalid_params.rs create mode 100644 sylvia/tests/ui/attributes/payload/invalid_params.stderr diff --git a/sylvia-derive/src/contract/communication/reply.rs b/sylvia-derive/src/contract/communication/reply.rs index 5864b039..86b0e0f0 100644 --- a/sylvia-derive/src/contract/communication/reply.rs +++ b/sylvia-derive/src/contract/communication/reply.rs @@ -6,11 +6,13 @@ use syn::{parse_quote, GenericParam, Ident, ItemImpl, Type}; use crate::crate_module; use crate::parser::attributes::msg::ReplyOn; -use crate::parser::{MsgType, ParsedSylviaAttributes, SylviaAttribute}; +use crate::parser::{MsgType, ParsedSylviaAttributes}; use crate::types::msg_field::MsgField; use crate::types::msg_variant::{MsgVariant, MsgVariants}; use crate::utils::emit_turbofish; +const NUMBER_OF_DATA_FIELDS: usize = 1; + pub struct Reply<'a> { source: &'a ItemImpl, generics: &'a [&'a GenericParam], @@ -173,7 +175,7 @@ impl<'a> ReplyVariants<'a> for MsgVariants<'a, GenericParam> { }, ) } - Some(existing_data) => existing_data.add_second_handler(handler), + Some(existing_data) => existing_data.merge(handler), None => reply_data.push(ReplyData::new(reply_id, handler, handler_id)), } }); @@ -198,9 +200,14 @@ struct ReplyData<'a> { impl<'a> ReplyData<'a> { pub fn new(reply_id: Ident, variant: &'a MsgVariant<'a>, handler_id: &'a Ident) -> Self { - let data = variant.fields().first(); - // Skip the first field reserved for the `data`. - let payload = variant.fields().iter().skip(1).collect::>(); + let data = variant.as_data_field(); + variant.validate_fields_attributes(); + let payload = variant.fields().iter(); + let payload = if data.is_some() || variant.msg_attr().reply_on() != ReplyOn::Success { + payload.skip(NUMBER_OF_DATA_FIELDS).collect::>() + } else { + payload.collect::>() + }; let method_name = variant.function_name(); let reply_on = variant.msg_attr().reply_on(); @@ -214,13 +221,15 @@ impl<'a> ReplyData<'a> { } /// Adds second handler to the reply data provdided their payload signature match. - pub fn add_second_handler(&mut self, new_handler: &'a MsgVariant<'a>) { + pub fn merge(&mut self, new_handler: &'a MsgVariant<'a>) { let (current_method_name, _) = match self.handlers.first() { Some(handler) => handler, _ => return, }; - if self.payload.len() != new_handler.fields().len() - 1 { + let new_reply_data = ReplyData::new(self.reply_id.clone(), new_handler, self.handler_id); + + if self.payload.len() != new_reply_data.payload.len() { emit_error!(current_method_name.span(), "Mismatched quantity of method parameters."; note = self.handler_id.span() => format!("Both `{}` handlers should have the same number of parameters.", self.handler_id); note = new_handler.function_name().span() => format!("Previous definition of {} handler.", self.handler_id) @@ -229,7 +238,7 @@ impl<'a> ReplyData<'a> { self.payload .iter() - .zip(new_handler.fields().iter().skip(1)) + .zip(new_reply_data.payload.iter()) .for_each(|(current_field, new_field)| { if current_field.ty() != new_field.ty() { @@ -377,6 +386,7 @@ impl<'a> ReplyData<'a> { let payload_values = self.payload.iter().map(|field| field.name()); let payload_deserialization = self.payload.emit_payload_deserialization(); let data_deserialization = self.data.map(DataField::emit_data_deserialization); + let data = self.data.map(|_| quote! { data, }); quote! { #sylvia ::cw_std::SubMsgResult::Ok(sub_msg_resp) => { @@ -385,7 +395,7 @@ impl<'a> ReplyData<'a> { #payload_deserialization #data_deserialization - #contract_turbofish ::new(). #method_name ((deps, env, gas_used, events, msg_responses).into(), data, #(#payload_values),* ) + #contract_turbofish ::new(). #method_name ((deps, env, gas_used, events, msg_responses).into(), #data #(#payload_values),* ) } } } @@ -462,6 +472,8 @@ impl<'a> ReplyData<'a> { trait ReplyVariant<'a> { fn as_variant_handlers_pair(&'a self) -> Vec<(&'a MsgVariant<'a>, &'a Ident)>; + fn as_data_field(&'a self) -> Option<&'a MsgField<'a>>; + fn validate_fields_attributes(&'a self); } impl<'a> ReplyVariant<'a> for MsgVariant<'a> { @@ -479,6 +491,28 @@ impl<'a> ReplyVariant<'a> for MsgVariant<'a> { variant_handler_id_pair } + + /// Validates attributes and returns `Some(MsgField)` if a field marked with `sv::data` attribute + /// is present and the `reply_on` attribute is set to `ReplyOn::Success`. + fn as_data_field(&'a self) -> Option<&'a MsgField<'a>> { + let data_attrs = self.fields().first().map(|field| { + ParsedSylviaAttributes::new(field.attrs().iter()) + .data + .is_some() + }); + match data_attrs { + Some(attrs) if attrs && self.msg_attr().reply_on() == ReplyOn::Success => { + self.fields().first() + } + _ => None, + } + } + + /// Validates if the fields attributes are correct. + fn validate_fields_attributes(&'a self) { + let field_attrs = self.fields().iter().flat_map(|field| field.attrs()); + ParsedSylviaAttributes::new(field_attrs); + } } pub trait DataField { @@ -489,10 +523,6 @@ impl DataField for MsgField<'_> { fn emit_data_deserialization(&self) -> TokenStream { let sylvia = crate_module(); let data = ParsedSylviaAttributes::new(self.attrs().iter()).data; - let is_data_attr = self - .attrs() - .iter() - .any(|attr| SylviaAttribute::new(attr) == Some(SylviaAttribute::Data)); let missing_data_err = "Missing reply data field."; let invalid_reply_data_err = quote! { format! {"Invalid reply data: {}\nSerde error while deserializing {}", data, err} @@ -555,7 +585,7 @@ impl DataField for MsgField<'_> { None => None, }; }, - None if is_data_attr => quote! { + _ => quote! { let data = match data { Some(data) => { #execute_data_deserialization @@ -565,13 +595,6 @@ impl DataField for MsgField<'_> { None => return Err(Into::into( #sylvia ::cw_std::StdError::generic_err( #missing_data_err ))), }; }, - _ => { - emit_error!(self.name().span(), "Invalid data usage."; - note = "Reply data should be marked with #[sv::data] attribute."; - note = "Remove this parameter or mark it with #[sv::data] attribute." - ); - quote! {} - } } } } @@ -616,8 +639,11 @@ impl PayloadFields for Vec<&MsgField<'_>> { } fn is_payload_marked(&self) -> bool { - self.iter() - .any(|field| field.contains_attribute(SylviaAttribute::Payload)) + self.iter().any(|field| { + ParsedSylviaAttributes::new(field.attrs().iter()) + .payload + .is_some() + }) } } diff --git a/sylvia-derive/src/parser/attributes/mod.rs b/sylvia-derive/src/parser/attributes/mod.rs index 215bda73..9d3bedf4 100644 --- a/sylvia-derive/src/parser/attributes/mod.rs +++ b/sylvia-derive/src/parser/attributes/mod.rs @@ -3,6 +3,7 @@ use data::DataFieldParams; use features::SylviaFeatures; +use payload::PayloadFieldParam; use proc_macro_error::emit_error; use syn::spanned::Spanned; use syn::{Attribute, MetaList, PathSegment}; @@ -15,6 +16,7 @@ pub mod features; pub mod messages; pub mod msg; pub mod override_entry_point; +pub mod payload; pub use attr::{MsgAttrForwarding, VariantAttrForwarding}; pub use custom::Custom; @@ -79,6 +81,7 @@ pub struct ParsedSylviaAttributes { pub msg_attrs_forward: Vec, pub sv_features: SylviaFeatures, pub data: Option, + pub payload: Option, } impl ParsedSylviaAttributes { @@ -90,6 +93,14 @@ impl ParsedSylviaAttributes { if let (Some(sylvia_attr), Ok(attr)) = (sylvia_attr, &attr_content) { result.match_attribute(&sylvia_attr, attr); + } else if sylvia_attr == Some(SylviaAttribute::Data) { + // The `sv::data` attribute can be used without parameters. + result.data = Some(DataFieldParams::default()); + } else if sylvia_attr == Some(SylviaAttribute::Payload) { + emit_error!( + attr.span(), "Missing parameters for `sv::payload`"; + note = "Expected `#[sv::payload(raw)]`" + ); } } @@ -172,10 +183,9 @@ impl ParsedSylviaAttributes { } } SylviaAttribute::Payload => { - emit_error!( - attr, "The attribute `sv::payload` used in wrong context"; - note = attr.span() => "The `sv::payload` should be used as a prefix for `Binary` payload."; - ); + if let Ok(payload) = PayloadFieldParam::new(attr) { + self.payload = Some(payload); + } } SylviaAttribute::Data => { if let Ok(data) = DataFieldParams::new(attr) { diff --git a/sylvia-derive/src/parser/attributes/payload.rs b/sylvia-derive/src/parser/attributes/payload.rs new file mode 100644 index 00000000..1f47951d --- /dev/null +++ b/sylvia-derive/src/parser/attributes/payload.rs @@ -0,0 +1,44 @@ +use proc_macro_error::emit_error; +use syn::parse::{Parse, ParseStream, Parser}; +use syn::{Error, Ident, MetaList, Result}; + +/// Type wrapping data parsed from `sv::payload` attribute. +#[derive(Default, Debug)] +pub struct PayloadFieldParam; + +impl PayloadFieldParam { + pub fn new(attr: &MetaList) -> Result { + let data = PayloadFieldParam::parse + .parse2(attr.tokens.clone()) + .map_err(|err| { + emit_error!(err.span(), err); + err + })?; + + Ok(data) + } +} + +impl Parse for PayloadFieldParam { + fn parse(input: ParseStream) -> Result { + let option: Ident = input.parse()?; + match option.to_string().as_str() { + "raw" => (), + _ => { + return Err(Error::new( + option.span(), + "Invalid payload parameter.\n= note: Expected [`raw`].\n", + )) + } + }; + + if !input.is_empty() { + return Err(Error::new( + input.span(), + "Unexpected tokens inside `sv::payload` attribute.\n= note: Expected parameters: [`raw`] `.\n", + )); + } + + Ok(Self) + } +} diff --git a/sylvia-derive/src/types/msg_field.rs b/sylvia-derive/src/types/msg_field.rs index 46bfffc4..ecaf9901 100644 --- a/sylvia-derive/src/types/msg_field.rs +++ b/sylvia-derive/src/types/msg_field.rs @@ -1,6 +1,5 @@ use crate::fold::StripSelfPath; use crate::parser::check_generics::{CheckGenerics, GetPath}; -use crate::parser::SylviaAttribute; use proc_macro2::TokenStream; use proc_macro_error::emit_error; use quote::quote; @@ -124,10 +123,4 @@ impl<'a> MsgField<'a> { pub fn attrs(&self) -> &'a Vec { self.attrs } - - pub fn contains_attribute(&self, sv_attr: SylviaAttribute) -> bool { - self.attrs - .iter() - .any(|attr| SylviaAttribute::new(attr) == Some(sv_attr)) - } } diff --git a/sylvia/tests/reply_data.rs b/sylvia/tests/reply_data.rs index f336720c..faea8699 100644 --- a/sylvia/tests/reply_data.rs +++ b/sylvia/tests/reply_data.rs @@ -122,7 +122,7 @@ impl Contract { // MultiTest version is released. // Payload is not currently forwarded in the MultiTest. // _instantiate_payload: InstantiatePayload, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> Result { let remote_addr = Addr::unchecked(data.contract_address); @@ -137,7 +137,7 @@ impl Contract { &self, _ctx: ReplyCtx, #[sv::data(instantiate, opt)] _data: Option, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> Result { Ok(Response::new()) } @@ -147,7 +147,7 @@ impl Contract { &self, _ctx: ReplyCtx, #[sv::data(raw, opt)] _data: Option, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> Result { Ok(Response::new()) } @@ -157,7 +157,7 @@ impl Contract { &self, _ctx: ReplyCtx, #[sv::data(raw)] _data: Binary, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> Result { Ok(Response::new()) } @@ -167,7 +167,7 @@ impl Contract { &self, _ctx: ReplyCtx, #[sv::data(opt)] _data: Option, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> Result { Ok(Response::new()) } @@ -177,13 +177,23 @@ impl Contract { &self, _ctx: ReplyCtx, #[sv::data] _data: String, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, + ) -> Result { + Ok(Response::new()) + } + + #[sv::msg(reply, reply_on=success)] + fn no_data( + &self, + _ctx: ReplyCtx, + #[sv::payload(raw)] _payload: Binary, ) -> Result { Ok(Response::new()) } } -mod tests { +#[test] +fn dispatch_replies() { use crate::noop_contract::sv::mt::CodeId as NoopCodeId; use crate::sv::mt::{CodeId, ContractProxy}; use crate::sv::{DATA_OPT_REPLY_ID, DATA_RAW_OPT_REPLY_ID, DATA_RAW_REPLY_ID, DATA_REPLY_ID}; @@ -192,88 +202,85 @@ mod tests { use sylvia::cw_multi_test::IntoBech32; use sylvia::multitest::App; - #[test] - fn dispatch_replies() { - let app = App::default(); - let code_id = CodeId::store_code(&app); - let noop_code_id = NoopCodeId::store_code(&app); - - let owner = "owner".into_bech32(); - let data = Some(to_json_binary(&String::from("some_data")).unwrap()); - let invalid_data = Some(Binary::from("InvalidData".as_bytes())); - - // Trigger remote instantiation reply - let contract = code_id - .instantiate(noop_code_id.code_id()) - .with_label("Contract") - .call(&owner) - .unwrap(); - - // Should forward `data` in every case - contract - .send_message_expecting_data(None, DATA_RAW_OPT_REPLY_ID) - .call(&owner) - .unwrap(); - - contract - .send_message_expecting_data(data.clone(), DATA_RAW_OPT_REPLY_ID) - .call(&owner) - .unwrap(); - - // Should forward `data` if `Some` and return error if `None` - let err = contract - .send_message_expecting_data(None, DATA_RAW_REPLY_ID) - .call(&owner) - .unwrap_err(); - assert_eq!( - err, - StdError::generic_err("Missing reply data field.").into() - ); - - contract - .send_message_expecting_data(data.clone(), DATA_RAW_REPLY_ID) - .call(&owner) - .unwrap(); - - // Should forward deserialized `data` if `Some` or None and return error if deserialization fails - contract - .send_message_expecting_data(None, DATA_OPT_REPLY_ID) - .call(&owner) - .unwrap(); - - let err = contract - .send_message_expecting_data(invalid_data.clone(), DATA_OPT_REPLY_ID) - .call(&owner) - .unwrap_err(); - assert_eq!( + let app = App::default(); + let code_id = CodeId::store_code(&app); + let noop_code_id = NoopCodeId::store_code(&app); + + let owner = "owner".into_bech32(); + let data = Some(to_json_binary(&String::from("some_data")).unwrap()); + let invalid_data = Some(Binary::from("InvalidData".as_bytes())); + + // Trigger remote instantiation reply + let contract = code_id + .instantiate(noop_code_id.code_id()) + .with_label("Contract") + .call(&owner) + .unwrap(); + + // Should forward `data` in every case + contract + .send_message_expecting_data(None, DATA_RAW_OPT_REPLY_ID) + .call(&owner) + .unwrap(); + + contract + .send_message_expecting_data(data.clone(), DATA_RAW_OPT_REPLY_ID) + .call(&owner) + .unwrap(); + + // Should forward `data` if `Some` and return error if `None` + let err = contract + .send_message_expecting_data(None, DATA_RAW_REPLY_ID) + .call(&owner) + .unwrap_err(); + assert_eq!( + err, + StdError::generic_err("Missing reply data field.").into() + ); + + contract + .send_message_expecting_data(data.clone(), DATA_RAW_REPLY_ID) + .call(&owner) + .unwrap(); + + // Should forward deserialized `data` if `Some` or None and return error if deserialization fails + contract + .send_message_expecting_data(None, DATA_OPT_REPLY_ID) + .call(&owner) + .unwrap(); + + let err = contract + .send_message_expecting_data(invalid_data.clone(), DATA_OPT_REPLY_ID) + .call(&owner) + .unwrap_err(); + assert_eq!( err, StdError::generic_err("Invalid reply data: SW52YWxpZERhdGE=\nSerde error while deserializing Error parsing into type alloc::string::String: Invalid type").into() ); - contract - .send_message_expecting_data(data.clone(), DATA_OPT_REPLY_ID) - .call(&owner) - .unwrap(); - - // Should forward deserialized `data` if `Some` and return error if `None` or if deserialization fails - let err = contract - .send_message_expecting_data(None, DATA_REPLY_ID) - .call(&owner) - .unwrap_err(); - assert_eq!( - err, - StdError::generic_err("Missing reply data field.").into() - ); - - let err = contract - .send_message_expecting_data(invalid_data, DATA_REPLY_ID) - .call(&owner) - .unwrap_err(); - assert_eq!(err, StdError::generic_err("Invalid reply data: SW52YWxpZERhdGE=\nSerde error while deserializing Error parsing into type alloc::string::String: Invalid type").into()); - - contract - .send_message_expecting_data(data, DATA_REPLY_ID) - .call(&owner) - .unwrap(); - } + contract + .send_message_expecting_data(data.clone(), DATA_OPT_REPLY_ID) + .call(&owner) + .unwrap(); + + // Should forward deserialized `data` if `Some` and return error if `None` or if deserialization fails + let err = contract + .send_message_expecting_data(None, DATA_REPLY_ID) + .call(&owner) + .unwrap_err(); + assert_eq!( + err, + StdError::generic_err("Missing reply data field.").into() + ); + + let err = contract + .send_message_expecting_data(invalid_data, DATA_REPLY_ID) + .call(&owner) + .unwrap_err(); + assert_eq!(err, StdError::generic_err("Invalid reply data: SW52YWxpZERhdGE=\nSerde error while deserializing Error parsing into type alloc::string::String: Invalid type").into()); + + contract + .send_message_expecting_data(data, DATA_REPLY_ID) + .call(&owner) + .unwrap(); } diff --git a/sylvia/tests/reply_dispatch.rs b/sylvia/tests/reply_dispatch.rs index 23569c5c..f9583aaf 100644 --- a/sylvia/tests/reply_dispatch.rs +++ b/sylvia/tests/reply_dispatch.rs @@ -219,7 +219,7 @@ where // MultiTest version is released. // Payload is not currently forwarded in the MultiTest. // _instantiate_payload: InstantiatePayload, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> Result, ContractError> { self.last_reply .save(ctx.deps.storage, &REMOTE_INSTANTIATED_REPLY_ID)?; @@ -237,7 +237,7 @@ where &self, ctx: ReplyCtx, #[sv::data(raw, opt)] _data: Option, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> Result, ContractError> { self.last_reply.save(ctx.deps.storage, &SUCCESS_REPLY_ID)?; @@ -249,7 +249,7 @@ where &self, ctx: ReplyCtx, _error: String, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> Result, ContractError> { self.last_reply.save(ctx.deps.storage, &FAILURE_REPLY_ID)?; @@ -261,7 +261,7 @@ where &self, ctx: ReplyCtx, _result: SubMsgResult, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, // _first_part_payload: u32, // _second_part_payload: String, ) -> Result, ContractError> { diff --git a/sylvia/tests/reply_generation.rs b/sylvia/tests/reply_generation.rs index afd65ad2..d5d39486 100644 --- a/sylvia/tests/reply_generation.rs +++ b/sylvia/tests/reply_generation.rs @@ -24,7 +24,7 @@ impl Contract { &self, _ctx: ReplyCtx, _result: SubMsgResult, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> StdResult { Ok(Response::new()) } @@ -35,18 +35,18 @@ impl Contract { &self, _ctx: ReplyCtx, _result: SubMsgResult, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> StdResult { Ok(Response::new()) } #[allow(dead_code)] #[sv::msg(reply, reply_on = success)] - fn reply_on( + fn two_handlers( &self, _ctx: ReplyCtx, #[sv::data(raw, opt)] _data: Option, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> StdResult { Ok(Response::new()) } @@ -57,18 +57,18 @@ impl Contract { &self, _ctx: ReplyCtx, _result: SubMsgResult, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> StdResult { Ok(Response::new()) } #[allow(dead_code)] - #[sv::msg(reply, handlers=[reply_on], reply_on = failure)] + #[sv::msg(reply, handlers=[two_handlers], reply_on = failure)] fn both_parameters( &self, _ctx: ReplyCtx, _error: String, - #[sv::payload] _payload: Binary, + #[sv::payload(raw)] _payload: Binary, ) -> StdResult { Ok(Response::new()) } @@ -81,7 +81,7 @@ fn reply_id_generation() { sv::CLEAN_REPLY_ID, sv::HANDLER_ONE_REPLY_ID, sv::HANDLER_TWO_REPLY_ID, - sv::REPLY_ON_REPLY_ID, + sv::TWO_HANDLERS_REPLY_ID, sv::REPLY_ON_ALWAYS_REPLY_ID, ] .iter() @@ -93,6 +93,6 @@ fn reply_id_generation() { assert_eq!(sv::CLEAN_REPLY_ID, 0); assert_eq!(sv::HANDLER_ONE_REPLY_ID, 1); assert_eq!(sv::HANDLER_TWO_REPLY_ID, 2); - assert_eq!(sv::REPLY_ON_REPLY_ID, 3); + assert_eq!(sv::TWO_HANDLERS_REPLY_ID, 3); assert_eq!(sv::REPLY_ON_ALWAYS_REPLY_ID, 4); } diff --git a/sylvia/tests/ui/attributes/data/missing_attribute.rs b/sylvia/tests/ui/attributes/data/invalid_params.rs similarity index 74% rename from sylvia/tests/ui/attributes/data/missing_attribute.rs rename to sylvia/tests/ui/attributes/data/invalid_params.rs index bed0fd3d..f58ce262 100644 --- a/sylvia/tests/ui/attributes/data/missing_attribute.rs +++ b/sylvia/tests/ui/attributes/data/invalid_params.rs @@ -1,7 +1,7 @@ #![allow(unused_imports)] use sylvia::contract; -use sylvia::cw_std::{Reply, Response, StdResult}; +use sylvia::cw_std::{Binary, Reply, Response, StdResult}; use sylvia::types::{InstantiateCtx, ReplyCtx}; pub struct Contract; @@ -22,9 +22,8 @@ impl Contract { fn reply( &self, _ctx: ReplyCtx, - // If the `data` attribute is missing, the data field should be omitted. - _data: Option, - param: String, + #[sv::data(invalid)] _data: Option, + _param: String, ) -> StdResult { Ok(Response::new()) } diff --git a/sylvia/tests/ui/attributes/data/invalid_params.stderr b/sylvia/tests/ui/attributes/data/invalid_params.stderr new file mode 100644 index 00000000..3df67754 --- /dev/null +++ b/sylvia/tests/ui/attributes/data/invalid_params.stderr @@ -0,0 +1,7 @@ +error: Invalid data parameter. + + = note: Expected one of [`raw`, `opt`, `instantiate`] comma separated. + --> tests/ui/attributes/data/invalid_params.rs:25:20 + | +25 | #[sv::data(invalid)] _data: Option, + | ^^^^^^^ diff --git a/sylvia/tests/ui/attributes/data/missing_attribute.stderr b/sylvia/tests/ui/attributes/data/missing_attribute.stderr deleted file mode 100644 index 64010df5..00000000 --- a/sylvia/tests/ui/attributes/data/missing_attribute.stderr +++ /dev/null @@ -1,9 +0,0 @@ -error: Invalid data usage. - - = note: Reply data should be marked with #[sv::data] attribute. - = note: Remove this parameter or mark it with #[sv::data] attribute. - - --> tests/ui/attributes/data/missing_attribute.rs:26:9 - | -26 | _data: Option, - | ^^^^^ diff --git a/sylvia/tests/ui/attributes/payload/invalid_params.rs b/sylvia/tests/ui/attributes/payload/invalid_params.rs new file mode 100644 index 00000000..79002bd8 --- /dev/null +++ b/sylvia/tests/ui/attributes/payload/invalid_params.rs @@ -0,0 +1,32 @@ +#![allow(unused_imports)] + +use sylvia::contract; +use sylvia::cw_std::{Binary, Reply, Response, StdResult}; +use sylvia::types::{InstantiateCtx, ReplyCtx}; + +pub struct Contract; + +#[contract] +#[sv::features(replies)] +impl Contract { + pub fn new() -> Self { + Self + } + + #[sv::msg(instantiate)] + pub fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult { + Ok(Response::new()) + } + + #[sv::msg(reply, reply_on=success)] + fn reply( + &self, + _ctx: ReplyCtx, + #[sv::data(raw, opt)] _data: Option, + #[sv::payload(invalid)] _param: Option, + ) -> StdResult { + Ok(Response::new()) + } +} + +fn main() {} diff --git a/sylvia/tests/ui/attributes/payload/invalid_params.stderr b/sylvia/tests/ui/attributes/payload/invalid_params.stderr new file mode 100644 index 00000000..0705ccd5 --- /dev/null +++ b/sylvia/tests/ui/attributes/payload/invalid_params.stderr @@ -0,0 +1,6 @@ +error: Invalid payload parameter. + = note: Expected [`raw`]. + --> tests/ui/attributes/payload/invalid_params.rs:26:23 + | +26 | #[sv::payload(invalid)] _param: Option, + | ^^^^^^^