Repository: CosmWasm/sylvia (rust)

Issue:
Add a `migrate` and `reply` entry points
Add generating the missing `migrate` and `reply` entry point defined on the contract.

It would be nice to have some additional layer on top of it to generate easier differential migrations, and also to connect replies to messages in some way, but for now we want to be able to define those directly.

Pull request description:
Migrate and reply support in sylvia
close #17 

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index 91a3906..b7d0194 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -864,6 +864,7 @@ dependencies = [
  "anyhow",
  "cosmwasm-schema",
  "cosmwasm-std",
+ "cw-multi-test",
  "cw-storage-plus",
  "konst",
  "schemars",
diff --git a/sylvia-derive/src/input.rs b/sylvia-derive/src/input.rs
index c19c385..8d6a509 100644
--- a/sylvia-derive/src/input.rs
+++ b/sylvia-derive/src/input.rs
@@ -116,6 +116,11 @@ impl<'a> ImplInput<'a> {
             self.emit_enum_msg(&Ident::new("ExecMsg", Span::mixed_site()), MsgType::Exec);
         let query_impl =
             self.emit_enum_msg(&Ident::new("QueryMsg", Span::mixed_site()), MsgType::Query);
+        let reply = self.emit_enum_msg(&Ident::new("ReplyMsg", Span::mixed_site()), MsgType::Reply);
+        let migrate = self.emit_enum_msg(
+            &Ident::new("MigrateMsg", Span::mixed_site()),
+            MsgType::Migrate,
+        );
         let exec = self.emit_glue_msg(&Ident::new("ExecMsg", Span::mixed_site()), MsgType::Exec);
         let query = self.emit_glue_msg(&Ident::new("QueryMsg", Span::mixed_site()), MsgType::Query);
 
@@ -126,6 +131,10 @@ impl<'a> ImplInput<'a> {
 
             #query_impl
 
+            #reply
+
+            #migrate
+
             #exec
 
             #query
diff --git a/sylvia-derive/src/message.rs b/sylvia-derive/src/message.rs
index f44b0bc..61db74f 100644
--- a/sylvia-derive/src/message.rs
+++ b/sylvia-derive/src/message.rs
@@ -599,7 +599,7 @@ impl<'a> MsgVariant<'a> {
         let fields = fields.iter().map(|field| field.name);
 
         match msg_attr {
-            Exec => quote! {
+            Exec | Migrate | Reply => quote! {
                 #name {
                     #(#fields,)*
                 } => contract.#function_name(ctx.into(), #(#args),*).map_err(Into::into)
diff --git a/sylvia-derive/src/parser.rs b/sylvia-derive/src/parser.rs
index 87c8d64..ab8808c 100644
--- a/sylvia-derive/src/parser.rs
+++ b/sylvia-derive/src/parser.rs
@@ -104,6 +104,8 @@ pub enum MsgType {
     Exec,
     Query,
     Instantiate,
+    Reply,
+    Migrate,
 }
 
 /// `#[msg(...)]` attribute for `interface` macro
@@ -111,6 +113,8 @@ pub enum MsgAttr {
     Exec,
     Query,
     Instantiate { name: Ident },
+    Reply,
+    Migrate,
 }
 
 impl MsgType {
@@ -121,6 +125,9 @@ impl MsgType {
             Exec | Instantiate => quote! {
                 (cosmwasm_std::DepsMut, cosmwasm_std::Env, cosmwasm_std::MessageInfo)
             },
+            Migrate | Reply => quote! {
+                (cosmwasm_std::DepsMut, cosmwasm_std::Env)
+            },
             Query => quote! {
                 (cosmwasm_std::Deps, cosmwasm_std::Env)
             },
@@ -134,10 +141,15 @@ impl MsgType {
         let sylvia = crate_module();
 
         match (self, msg_type) {
-            (Exec, Some(msg_type)) | (Instantiate, Some(msg_type)) => quote! {
-                std::result::Result<#sylvia ::cw_std::Response<#msg_type>, #err_type>
-            },
-            (Exec, None) | (Instantiate, None) => quote! {
+            (Exec, Some(msg_type))
+            | (Instantiate, Some(msg_type))
+            | (Migrate, Some(msg_type))
+            | (Reply, Some(msg_type)) => {
+                quote! {
+                    std::result::Result<#sylvia ::cw_std::Response<#msg_type>, #err_type>
+                }
+            }
+            (Exec, None) | (Instantiate, None) | (Migrate, None) | (Reply, None) => quote! {
                 std::result::Result<#sylvia ::cw_std::Response, #err_type>
             },
 
@@ -198,6 +210,8 @@ impl MsgAttr {
             Exec => MsgType::Exec,
             Query => MsgType::Query,
             Instantiate { .. } => MsgType::Instantiate,
+            Reply => MsgType::Reply,
+            Migrate => MsgType::Migrate,
         }
     }
 }
@@ -214,10 +228,14 @@ impl Parse for MsgAttr {
             Ok(Self::Query)
         } else if ty == "instantiate" {
             Self::parse_instantiate(content)
+        } else if ty == "reply" {
+            Ok(Self::Reply)
+        } else if ty == "migrate" {
+            Ok(Self::Migrate)
         } else {
             Err(Error::new(
                 ty.span(),
-                "Invalid message type, expected one of: `exec`, `query`, `instantiate`",
+                "Invalid message type, expected one of: `exec`, `query`, `instantiate`, `reply`, `migrate`",
             ))
         }
     }
diff --git a/sylvia/Cargo.toml b/sylvia/Cargo.toml
index c9c9f4c..afce997 100644
--- a/sylvia/Cargo.toml
+++ b/sylvia/Cargo.toml
@@ -23,3 +23,4 @@ konst = "0.2"
 [dev-dependencies]
 anyhow = "1"
 cw-storage-plus = "0.15.1"
+cw-multi-test = "0.15.1"


Test patch:
diff --git a/sylvia/tests/dispatching.rs b/sylvia/tests/dispatching.rs
index f6b7158..479870a 100644
--- a/sylvia/tests/dispatching.rs
+++ b/sylvia/tests/dispatching.rs
@@ -93,12 +93,12 @@ fn dispatch() {
     let env = mock_env();
     let info = mock_info("owner", &[]);
 
-    let resp = InterfaceExecMsg::NoArgsExecution {}
+    let resp = ExecMsg::NoArgsExecution {}
         .dispatch(&contract, (deps.as_mut(), env.clone(), info.clone()))
         .unwrap();
     assert_eq!(resp, Response::new());
 
-    let resp = InterfaceExecMsg::ArgumentedExecution {
+    let resp = ExecMsg::ArgumentedExecution {
         addr: Addr::unchecked("addr1"),
         coef: Decimal::percent(30),
         desc: "True".to_owned(),
@@ -107,7 +107,7 @@ fn dispatch() {
     .unwrap();
     assert_eq!(resp, Response::new());
 
-    let resp = InterfaceExecMsg::ArgumentedExecution {
+    let resp = ExecMsg::ArgumentedExecution {
         addr: Addr::unchecked("addr2"),
         coef: Decimal::percent(70),
         desc: "False".to_owned(),
@@ -116,12 +116,12 @@ fn dispatch() {
     .unwrap();
     assert_eq!(resp, Response::new());
 
-    let resp = InterfaceQueryMsg::NoArgsQuery {}
+    let resp = QueryMsg::NoArgsQuery {}
         .dispatch(&contract, (deps.as_ref(), env.clone()))
         .unwrap();
     let _resp: EmptyQueryResponse = from_binary(&resp).unwrap();
 
-    let resp = InterfaceQueryMsg::ArgumentedQuery {
+    let resp = QueryMsg::ArgumentedQuery {
         user: Addr::unchecked("addr2"),
     }
     .dispatch(&contract, (deps.as_ref(), env))
diff --git a/sylvia/tests/messages_generation.rs b/sylvia/tests/messages_generation.rs
index 1434082..5fd30b7 100644
--- a/sylvia/tests/messages_generation.rs
+++ b/sylvia/tests/messages_generation.rs
@@ -1,13 +1,13 @@
-use cosmwasm_std::{Addr, Decimal, Deps, DepsMut, Env, MessageInfo, Response, StdError};
+use cosmwasm_std::{Addr, Decimal, Deps, DepsMut, Env, MessageInfo, Response, StdError, StdResult};
 
-use sylvia::interface;
+use sylvia::{contract, interface};
 
 #[derive(
     serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, schemars::JsonSchema,
 )]
 pub struct QueryResult;
 
-#[interface]
+#[interface(module=interface)]
 pub trait Interface {
     type Error: From<StdError>;
 
@@ -30,27 +30,111 @@ pub trait Interface {
     fn argumented_query(&self, ctx: (Deps, Env), user: Addr) -> Result<QueryResult, Self::Error>;
 }
 
+pub struct Contract {}
+
+#[contract(module=contract, error=StdError)]
+impl Contract {
+    #[msg(instantiate)]
+    pub fn instantiate(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+
+    #[msg(migrate)]
+    pub fn migrate(&self, _ctx: (DepsMut, Env)) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+
+    #[msg(reply)]
+    pub fn reply(&self, _ctx: (DepsMut, Env)) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+
+    #[msg(exec)]
+    fn no_args_execution(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+
+    #[allow(unused_variables)]
+    #[msg(exec)]
+    fn argumented_execution(
+        &self,
+        _ctx: (DepsMut, Env, MessageInfo),
+        addr: Addr,
+        coef: Decimal,
+        desc: String,
+    ) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+
+    #[msg(query)]
+    fn no_args_query(&self, _ctx: (Deps, Env)) -> StdResult<QueryResult> {
+        Ok(QueryResult {})
+    }
+
+    #[allow(unused_variables)]
+    #[msg(query)]
+    fn argumented_query(&self, _ctx: (Deps, Env), user: Addr) -> StdResult<QueryResult> {
+        Ok(QueryResult {})
+    }
+}
+
+#[test]
+fn interface_messages_constructible() {
+    let no_args_exec = interface::ExecMsg::NoArgsExecution {};
+    let _argumented_exec = interface::ExecMsg::ArgumentedExecution {
+        addr: Addr::unchecked("owner"),
+        coef: Decimal::percent(10),
+        desc: "Some description".to_owned(),
+    };
+    let no_args_query = interface::QueryMsg::NoArgsQuery {};
+    let _argumented_query = interface::QueryMsg::ArgumentedQuery {
+        user: Addr::unchecked("owner"),
+    };
+
+    // Ensure no extra variants are generated
+    match no_args_exec {
+        interface::ExecMsg::NoArgsExecution {} => (),
+        interface::ExecMsg::ArgumentedExecution { .. } => (),
+    }
+
+    match no_args_query {
+        interface::QueryMsg::NoArgsQuery {} => (),
+        interface::QueryMsg::ArgumentedQuery { .. } => (),
+    }
+}
+
 #[test]
-fn messages_constructible() {
-    let no_args_exec = InterfaceExecMsg::NoArgsExecution {};
-    let _argumented_exec = InterfaceExecMsg::ArgumentedExecution {
+fn contract_messages_constructible() {
+    let no_args_exec = contract::ExecMsg::NoArgsExecution {};
+    let _argumented_exec = contract::ExecMsg::ArgumentedExecution {
         addr: Addr::unchecked("owner"),
         coef: Decimal::percent(10),
         desc: "Some description".to_owned(),
     };
-    let no_args_query = InterfaceQueryMsg::NoArgsQuery {};
-    let _argumented_query = InterfaceQueryMsg::ArgumentedQuery {
+    let no_args_query = contract::QueryMsg::NoArgsQuery {};
+    let _argumented_query = contract::QueryMsg::ArgumentedQuery {
         user: Addr::unchecked("owner"),
     };
+    let _ = contract::InstantiateMsg {};
+    let reply = contract::ReplyMsg::Reply {};
+    let migrate = contract::MigrateMsg::Migrate {};
 
     // Ensure no extra variants are generated
     match no_args_exec {
-        InterfaceExecMsg::NoArgsExecution {} => (),
-        InterfaceExecMsg::ArgumentedExecution { .. } => (),
+        contract::ExecMsg::NoArgsExecution {} => (),
+        contract::ExecMsg::ArgumentedExecution { .. } => (),
     }
 
     match no_args_query {
-        InterfaceQueryMsg::NoArgsQuery {} => (),
-        InterfaceQueryMsg::ArgumentedQuery { .. } => (),
+        contract::QueryMsg::NoArgsQuery {} => (),
+        contract::QueryMsg::ArgumentedQuery { .. } => (),
+    }
+
+    match reply {
+        contract::ReplyMsg::Reply {} => (),
+    }
+
+    match migrate {
+        contract::MigrateMsg::Migrate {} => (),
     }
 }
diff --git a/sylvia/tests/reply.rs b/sylvia/tests/reply.rs
new file mode 100644
index 0000000..437e473
--- /dev/null
+++ b/sylvia/tests/reply.rs
@@ -0,0 +1,268 @@
+use anyhow::{bail, Result as AnyResult};
+use cosmwasm_std::{
+    from_slice, to_binary, Addr, Deps, DepsMut, Empty, Env, MessageInfo, Response, StdError,
+    StdResult, SubMsg, SubMsgResult, WasmMsg,
+};
+use cw_multi_test::{App, Contract, Executor};
+use cw_storage_plus::Item;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use sylvia::contract;
+
+pub const VOTE_CODE_ID: Item<u64> = Item::new("vote_code_id");
+pub const VOTES_COUNT: Item<u32> = Item::new("votes_count");
+
+pub const VOTE_INSTANTIATE_ID: u64 = 1;
+
+pub struct AdminContract {}
+
+pub struct VoteContract {}
+
+#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct VotesCountResp {
+    pub votes_count: u32,
+}
+
+#[derive(Serialize, Deserialize, Eq, PartialEq, Debug, Clone, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub struct ProposeAdminResp {
+    pub vote_addr: Addr,
+}
+
+#[contract(module=admin, error=StdError)]
+impl AdminContract {
+    #[msg(instantiate)]
+    pub fn instantiate(
+        &self,
+        ctx: (DepsMut, Env, MessageInfo),
+        vote_code_id: u64,
+    ) -> StdResult<Response> {
+        let (deps, ..) = ctx;
+        VOTE_CODE_ID.save(deps.storage, &vote_code_id)?;
+        VOTES_COUNT.save(deps.storage, &0)?;
+        Ok(Response::new())
+    }
+
+    #[msg(exec)]
+    pub fn create_vote(&self, ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
+        let (deps, _env, info) = ctx;
+        let msg = vote::InstantiateMsg {};
+
+        let msg = WasmMsg::Instantiate {
+            admin: None,
+            code_id: VOTE_CODE_ID.load(deps.storage)?,
+            msg: to_binary(&msg)?,
+            funds: vec![],
+            label: format!("admin-{}", info.sender),
+        };
+
+        let resp = Response::new()
+            .add_submessage(SubMsg::reply_on_success(msg, VOTE_INSTANTIATE_ID))
+            .add_attribute("action", "create_vote")
+            .add_attribute("sender", info.sender);
+
+        Ok(resp)
+    }
+
+    #[msg(query)]
+    pub fn votes_count(&self, ctx: (Deps, Env)) -> StdResult<VotesCountResp> {
+        let (deps, _) = ctx;
+        let votes_count = VOTES_COUNT.load(deps.storage)?;
+        let resp = VotesCountResp { votes_count };
+        Ok(resp)
+    }
+
+    #[msg(reply)]
+    pub fn reply(&self, ctx: (DepsMut, Env), _msg: SubMsgResult) -> StdResult<Response> {
+        let (deps, _) = ctx;
+
+        VOTES_COUNT.update(deps.storage, |mut c| -> StdResult<_> {
+            c += 1;
+            Ok(c)
+        })?;
+
+        Ok(Response::new())
+    }
+}
+
+#[contract(module=vote, error=StdError)]
+impl VoteContract {
+    #[msg(instantiate)]
+    pub fn instantiate(&self, _ctx: (DepsMut, Env, MessageInfo)) -> StdResult<Response> {
+        Ok(Response::new())
+    }
+}
+
+impl Contract<Empty> for AdminContract {
+    fn execute(
+        &self,
+        deps: cosmwasm_std::DepsMut<Empty>,
+        env: cosmwasm_std::Env,
+        info: cosmwasm_std::MessageInfo,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        from_slice::<self::admin::ContractExecMsg>(&msg)?
+            .dispatch(self, (deps, env, info))
+            .map_err(Into::into)
+    }
+
+    fn instantiate(
+        &self,
+        deps: cosmwasm_std::DepsMut<Empty>,
+        env: cosmwasm_std::Env,
+        info: cosmwasm_std::MessageInfo,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        from_slice::<self::admin::InstantiateMsg>(&msg)?
+            .dispatch(self, (deps, env, info))
+            .map_err(Into::into)
+    }
+
+    fn query(
+        &self,
+        deps: cosmwasm_std::Deps<Empty>,
+        env: cosmwasm_std::Env,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Binary> {
+        from_slice::<self::admin::ContractQueryMsg>(&msg)?
+            .dispatch(self, (deps, env))
+            .map_err(Into::into)
+    }
+
+    fn sudo(
+        &self,
+        _deps: cosmwasm_std::DepsMut<Empty>,
+        _env: cosmwasm_std::Env,
+        _msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        bail!("sudo not implemented for contract")
+    }
+
+    fn reply(
+        &self,
+        deps: cosmwasm_std::DepsMut<Empty>,
+        env: cosmwasm_std::Env,
+        msg: cosmwasm_std::Reply,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        match msg.id {
+            VOTE_INSTANTIATE_ID => self.reply((deps, env), msg.result).map_err(Into::into),
+            _ => Err(anyhow::Error::new(StdError::generic_err(
+                "unknown reply id",
+            ))),
+        }
+    }
+
+    fn migrate(
+        &self,
+        _deps: cosmwasm_std::DepsMut<Empty>,
+        _env: cosmwasm_std::Env,
+        _msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        bail!("migrate not implemented for contract")
+    }
+}
+
+impl Contract<Empty> for VoteContract {
+    fn execute(
+        &self,
+        deps: cosmwasm_std::DepsMut<Empty>,
+        env: cosmwasm_std::Env,
+        info: cosmwasm_std::MessageInfo,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        from_slice::<self::vote::ContractExecMsg>(&msg)?
+            .dispatch(self, (deps, env, info))
+            .map_err(Into::into)
+    }
+
+    fn instantiate(
+        &self,
+        deps: cosmwasm_std::DepsMut<Empty>,
+        env: cosmwasm_std::Env,
+        info: cosmwasm_std::MessageInfo,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        from_slice::<self::vote::InstantiateMsg>(&msg)?
+            .dispatch(self, (deps, env, info))
+            .map_err(Into::into)
+    }
+
+    fn query(
+        &self,
+        deps: cosmwasm_std::Deps<Empty>,
+        env: cosmwasm_std::Env,
+        msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Binary> {
+        from_slice::<self::vote::ContractQueryMsg>(&msg)?
+            .dispatch(self, (deps, env))
+            .map_err(Into::into)
+    }
+
+    fn sudo(
+        &self,
+        _deps: cosmwasm_std::DepsMut<Empty>,
+        _env: cosmwasm_std::Env,
+        _msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        bail!("sudo not implemented for contract")
+    }
+
+    fn reply(
+        &self,
+        _deps: cosmwasm_std::DepsMut<Empty>,
+        _env: cosmwasm_std::Env,
+        _msg: cosmwasm_std::Reply,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        bail!("reply not implemented for contract")
+    }
+
+    fn migrate(
+        &self,
+        _deps: cosmwasm_std::DepsMut<Empty>,
+        _env: cosmwasm_std::Env,
+        _msg: Vec<u8>,
+    ) -> AnyResult<cosmwasm_std::Response<Empty>> {
+        bail!("migrate not implemented for contract")
+    }
+}
+
+#[test]
+fn update_count_in_reply() {
+    let mut app = App::default();
+    let admin_code_id = app.store_code(Box::new(AdminContract {}));
+    let vote_code_id = app.store_code(Box::new(VoteContract {}));
+
+    let admin = app
+        .instantiate_contract(
+            admin_code_id,
+            Addr::unchecked("owner"),
+            &admin::InstantiateMsg { vote_code_id },
+            &[],
+            "admin",
+            None,
+        )
+        .unwrap();
+
+    let resp: VotesCountResp = app
+        .wrap()
+        .query_wasm_smart(admin.clone(), &admin::QueryMsg::VotesCount {})
+        .unwrap();
+
+    assert_eq!(resp.votes_count, 0);
+
+    app.execute_contract(
+        Addr::unchecked("owner"),
+        admin.clone(),
+        &admin::ExecMsg::CreateVote {},
+        &[],
+    )
+    .unwrap();
+
+    let resp: VotesCountResp = app
+        .wrap()
+        .query_wasm_smart(admin, &admin::QueryMsg::VotesCount {})
+        .unwrap();
+
+    assert_eq!(resp.votes_count, 1);
+}
diff --git a/sylvia/tests/separate_module.rs b/sylvia/tests/separate_module.rs
index aff80ca..d07260b 100644
--- a/sylvia/tests/separate_module.rs
+++ b/sylvia/tests/separate_module.rs
@@ -32,14 +32,14 @@ pub trait Interface {
 
 #[test]
 fn messages_constructible() {
-    let _no_args_exec = msg::InterfaceExecMsg::NoArgsExecution {};
-    let _argumented_exec = msg::InterfaceExecMsg::ArgumentedExecution {
+    let _no_args_exec = msg::ExecMsg::NoArgsExecution {};
+    let _argumented_exec = msg::ExecMsg::ArgumentedExecution {
         addr: Addr::unchecked("owner"),
         coef: Decimal::percent(10),
         desc: "Some description".to_owned(),
     };
-    let _no_args_query = msg::InterfaceQueryMsg::NoArgsQuery {};
-    let _argumented_query = msg::InterfaceQueryMsg::ArgumentedQuery {
+    let _no_args_query = msg::QueryMsg::NoArgsQuery {};
+    let _argumented_query = msg::QueryMsg::ArgumentedQuery {
         user: Addr::unchecked("owner"),
     };
 }