Repository: CosmWasm/sylvia
Pull request #437 (2024-09-26 13:27:58)

feat: Add InstantiateBuilder and SubMsg trait

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) Report
Attention: Patch coverage is `90.24390%` with `16 lines` in your changes missing coverage. Please review.
> Project coverage is 72.24%. Comparing base [(`72ebc77`)](https://app.codecov.io/gh/CosmWasm/sylvia/commit/72ebc770d14ee613ddf06f9dd70325721acff46d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) to head [(`d6c31de`)](https://app.codecov.io/gh/CosmWasm/sylvia/commit/d6c31deb1d7e252cc186c44de6528926fd73645b?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm).

| [Files with missing lines](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) | Patch % | Lines |
|---|---|---|
| [sylvia/src/builder/instantiate.rs](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?src=pr&el=tree&filepath=sylvia%2Fsrc%2Fbuilder%2Finstantiate.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-c3lsdmlhL3NyYy9idWlsZGVyL2luc3RhbnRpYXRlLnJz) | 47.82% | [12 Missing :warning: ](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) |
| [sylvia-derive/src/utils.rs](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?src=pr&el=tree&filepath=sylvia-derive%2Fsrc%2Futils.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-c3lsdmlhLWRlcml2ZS9zcmMvdXRpbHMucnM=) | 75.00% | [2 Missing :warning: ](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) |
| [sylvia-derive/src/contract.rs](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?src=pr&el=tree&filepath=sylvia-derive%2Fsrc%2Fcontract.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-c3lsdmlhLWRlcml2ZS9zcmMvY29udHJhY3QucnM=) | 93.33% | [1 Missing :warning: ](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) |
| [sylvia-derive/src/contract/communication/reply.rs](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?src=pr&el=tree&filepath=sylvia-derive%2Fsrc%2Fcontract%2Fcommunication%2Freply.rs&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#diff-c3lsdmlhLWRlcml2ZS9zcmMvY29udHJhY3QvY29tbXVuaWNhdGlvbi9yZXBseS5ycw==) | 98.48% | [1 Missing :warning: ](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) |

<details><summary>Additional details and impacted files</summary>


```diff
@@               Coverage Diff                @@
##           feat/replies     #437      +/-   ##
================================================
+ Coverage         71.23%   72.24%   +1.01%     
================================================
  Files                54       56       +2     
  Lines              3299     3445     +146     
================================================
+ Hits               2350     2489     +139     
- Misses              949      956       +7     
```

| [Flag](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) | Coverage Δ | |
|---|---|---|
| [](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm) | `72.24% <90.24%> (?)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/CosmWasm/sylvia/pull/437?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=CosmWasm).


Patch:
diff --git a/sylvia-derive/src/contract.rs b/sylvia-derive/src/contract.rs
index ae294c4..b52c980 100644
--- a/sylvia-derive/src/contract.rs
+++ b/sylvia-derive/src/contract.rs
@@ -1,6 +1,7 @@
 use communication::api::Api;
 use communication::enum_msg::EnumMessage;
 use communication::executor::Executor;
+use communication::instantiate_builder::InstantiateBuilder;
 use communication::querier::Querier;
 use communication::reply::Reply;
 use communication::struct_msg::StructMessage;
@@ -83,6 +84,7 @@ impl<'a> ContractInput<'a> {
         let querier = self.emit_querier();
         let executor = self.emit_executor();
         let reply = self.emit_reply();
+        let instantiate_builder = self.emit_instantiate_builder_trait();
 
         quote! {
             pub mod sv {
@@ -99,6 +101,8 @@ impl<'a> ContractInput<'a> {
                 #reply
 
                 #contract_api
+
+                #instantiate_builder
             }
         }
     }
@@ -190,4 +194,26 @@ impl<'a> ContractInput<'a> {
             quote! {}
         }
     }
+
+    fn emit_instantiate_builder_trait(&self) -> TokenStream {
+        let item = self.item;
+        let variants = MsgVariants::new(
+            item.as_variants(),
+            MsgType::Instantiate,
+            &self.generics,
+            &item.generics.where_clause,
+        );
+        let where_clause = variants.where_clause();
+
+        match variants.get_only_variant() {
+            Some(variant) => InstantiateBuilder::new(
+                *item.self_ty.clone(),
+                variants.used_generics(),
+                &where_clause,
+                variant,
+            )
+            .emit(),
+            None => quote! {},
+        }
+    }
 }
diff --git a/sylvia-derive/src/contract/communication/instantiate_builder.rs b/sylvia-derive/src/contract/communication/instantiate_builder.rs
new file mode 100644
index 0000000..aee4bf5
--- /dev/null
+++ b/sylvia-derive/src/contract/communication/instantiate_builder.rs
@@ -0,0 +1,68 @@
+use convert_case::Case;
+use proc_macro2::{Ident, TokenStream};
+use quote::quote;
+use syn::{GenericParam, Type, WhereClause};
+
+use crate::crate_module;
+use crate::parser::MsgType;
+use crate::types::msg_variant::MsgVariant;
+use crate::utils::{get_ident_from_type, SvCasing};
+
+pub struct InstantiateBuilder<'a> {
+    contract: Type,
+    used_generics: &'a [&'a GenericParam],
+    where_clause: &'a Option<WhereClause>,
+    instantiate_variant: &'a MsgVariant<'a>,
+}
+
+impl<'a> InstantiateBuilder<'a> {
+    pub fn new(
+        contract: Type,
+        used_generics: &'a [&'a syn::GenericParam],
+        where_clause: &'a Option<WhereClause>,
+        instantiate_variant: &'a MsgVariant<'a>,
+    ) -> Self {
+        Self {
+            contract,
+            used_generics,
+            where_clause,
+            instantiate_variant,
+        }
+    }
+
+    pub fn emit(&self) -> TokenStream {
+        let sylvia = crate_module();
+        let Self {
+            contract,
+            used_generics,
+            where_clause,
+            instantiate_variant,
+        } = self;
+
+        let contract_name = get_ident_from_type(contract);
+        let msg_type = MsgType::Instantiate;
+
+        let trait_name = Ident::new(
+            &format!("{}InstantiateBuilder", &contract_name.to_string()),
+            contract_name.span(),
+        );
+        let method_name = contract_name.to_case(Case::Snake);
+        let fields_names = instantiate_variant.as_fields_names();
+        let parameters = instantiate_variant.emit_method_field();
+        let msg_name = msg_type.emit_msg_name();
+
+        quote! {
+            pub trait #trait_name {
+                fn #method_name < #(#used_generics),* > (code_id: u64, #(#parameters),* ) -> #sylvia ::cw_std::StdResult < #sylvia ::builder::instantiate::InstantiateBuilder> #where_clause;
+            }
+
+            impl #trait_name for #sylvia ::builder::instantiate::InstantiateBuilder {
+                fn #method_name < #(#used_generics),* > (code_id: u64, #(#parameters,)* ) -> #sylvia ::cw_std::StdResult< #sylvia ::builder::instantiate::InstantiateBuilder> #where_clause {
+                    let msg = #msg_name ::< #(#used_generics),* > ::new( #(#fields_names),* );
+                    let msg = #sylvia ::cw_std::to_json_binary(&msg)?;
+                    Ok( #sylvia ::builder::instantiate::InstantiateBuilder::new(msg, code_id))
+                }
+            }
+        }
+    }
+}
diff --git a/sylvia-derive/src/contract/communication/mod.rs b/sylvia-derive/src/contract/communication/mod.rs
index a661895..8653ff9 100644
--- a/sylvia-derive/src/contract/communication/mod.rs
+++ b/sylvia-derive/src/contract/communication/mod.rs
@@ -1,6 +1,7 @@
 pub mod api;
 pub mod enum_msg;
 pub mod executor;
+pub mod instantiate_builder;
 pub mod querier;
 pub mod reply;
 pub mod struct_msg;
diff --git a/sylvia-derive/src/contract/communication/reply.rs b/sylvia-derive/src/contract/communication/reply.rs
index 7c40350..28c6764 100644
--- a/sylvia-derive/src/contract/communication/reply.rs
+++ b/sylvia-derive/src/contract/communication/reply.rs
@@ -2,14 +2,13 @@ use convert_case::{Case, Casing};
 use proc_macro2::TokenStream;
 use proc_macro_error::emit_error;
 use quote::quote;
-use syn::fold::Fold;
 use syn::{parse_quote, GenericParam, Ident, ItemImpl, Type};
 
 use crate::crate_module;
-use crate::fold::StripGenerics;
 use crate::parser::attributes::msg::ReplyOn;
 use crate::parser::MsgType;
 use crate::types::msg_variant::{MsgVariant, MsgVariants};
+use crate::utils::emit_turbofish;
 
 pub struct Reply<'a> {
     source: &'a ItemImpl,
@@ -35,11 +34,14 @@ impl<'a> Reply<'a> {
     pub fn emit(&self) -> TokenStream {
         let unique_handlers: Vec<_> = self.emit_reply_ids().collect();
         let dispatch = self.emit_dispatch();
+        let sub_msg_trait = self.emit_sub_msg_trait();
 
         quote! {
             #(#unique_handlers)*
 
             #dispatch
+
+            #sub_msg_trait
         }
     }
 
@@ -101,6 +103,74 @@ impl<'a> Reply<'a> {
             }
         })
     }
+
+    /// Generates `SubMsgMethods` trait with method for every reply id.
+    fn emit_sub_msg_trait(&self) -> TokenStream {
+        let Self { reply_data, .. } = self;
+
+        let sylvia = crate_module();
+
+        let methods_declaration = reply_data.iter().map(|data| {
+            let method_name = &data.handler_name;
+
+            quote! {
+                fn #method_name (self) -> #sylvia ::cw_std::SubMsg<CustomMsgT>;
+            }
+        });
+
+        let submsg_methods_implementation = reply_data.iter().map(|data| {
+            let method_name = &data.handler_name;
+            let reply_on = data.emit_cw_reply_on();
+            let reply_id = &data.reply_id;
+
+            quote! {
+                fn #method_name (self) -> #sylvia ::cw_std::SubMsg<CustomMsgT> {
+                    #sylvia ::cw_std::SubMsg {
+                        reply_on: #reply_on ,
+                        id: #reply_id ,
+                        ..self
+                    }
+                }
+            }
+        });
+
+        let wasmmsg_methods_implementation = reply_data.iter().map(|data| {
+            let method_name = &data.handler_name;
+            let reply_on = data.emit_cw_reply_on();
+            let reply_id = &data.reply_id;
+
+            quote! {
+                fn #method_name (self) -> #sylvia ::cw_std::SubMsg<CustomMsgT> {
+                    #sylvia ::cw_std::SubMsg {
+                        reply_on: #reply_on ,
+                        id: #reply_id ,
+                        msg: self.into(),
+                        payload: Default::default(),
+                        gas_limit: None,
+                    }
+                }
+            }
+        });
+        let cosmosmsg_methods_implementation = wasmmsg_methods_implementation.clone();
+
+        quote! {
+            pub trait SubMsgMethods<CustomMsgT> {
+                #(#methods_declaration)*
+            }
+
+            impl<CustomMsgT> SubMsgMethods<CustomMsgT> for #sylvia ::cw_std::SubMsg<CustomMsgT> {
+                #(#submsg_methods_implementation)*
+            }
+
+            impl<CustomMsgT> SubMsgMethods<CustomMsgT> for #sylvia ::cw_std::WasmMsg {
+                #(#wasmmsg_methods_implementation)*
+            }
+
+            impl<CustomMsgT> SubMsgMethods<CustomMsgT> for #sylvia ::cw_std::CosmosMsg<CustomMsgT> {
+                #(#cosmosmsg_methods_implementation)*
+            }
+        }
+    }
 }
 
 trait ReplyVariants<'a> {
@@ -115,7 +185,7 @@ impl<'a> ReplyVariants<'a> for MsgVariants<'a, GenericParam> {
 
         self.variants()
             .flat_map(ReplyVariant::as_reply_data)
-            .for_each(|(reply_id, function_name, reply_on)| {
+            .for_each(|(reply_id,  handler_name,function_name, reply_on)| {
                 match reply_data
                     .iter_mut()
                     .find(|existing_data| existing_data.reply_id == reply_id)
@@ -135,7 +205,7 @@ impl<'a> ReplyVariants<'a> for MsgVariants<'a, GenericParam> {
                         )
                     }
                     Some(existing_data) => existing_data.handlers.push((function_name, reply_on)),
-                    None => reply_data.push(ReplyData::new(reply_id, function_name, reply_on)),
+                    None => reply_data.push(ReplyData::new(reply_id, function_name, reply_on, handler_name)),
                 }
             });
 
@@ -146,28 +216,31 @@ impl<'a> ReplyVariants<'a> for MsgVariants<'a, GenericParam> {
 /// Maps single reply id with its handlers.
 struct ReplyData<'a> {
     pub reply_id: Ident,
+    pub handler_name: &'a Ident,
     pub handlers: Vec<(&'a Ident, ReplyOn)>,
 }
 
 impl<'a> ReplyData<'a> {
-    pub fn new(reply_id: Ident, function_name: &'a Ident, reply_on: ReplyOn) -> Self {
+    pub fn new(
+        reply_id: Ident,
+        function_name: &'a Ident,
+        reply_on: ReplyOn,
+        handler_name: &'a Ident,
+    ) -> Self {
         Self {
             reply_id,
+            handler_name,
             handlers: vec![(function_name, reply_on)],
         }
     }
 
     /// Emits success and failure match arms for a single `ReplyId`.
     fn emit_match_arms(&self, contract: &Type, generics: &[&GenericParam]) -> TokenStream {
-        let Self { reply_id, handlers } = self;
-
-        let contract_turbofish: Type = if !generics.is_empty() {
-            let contract_name = StripGenerics.fold_type((contract.clone()).clone());
-            parse_quote! { #contract_name :: < #(#generics),* > }
-        } else {
-            parse_quote! { #contract }
-        };
+        let Self {
+            reply_id, handlers, ..
+        } = self;
 
+        let contract_turbofish = emit_turbofish(contract, generics);
         let success_match_arm = emit_success_match_arm(handlers, &contract_turbofish);
         let failure_match_arm = emit_failure_match_arm(handlers, &contract_turbofish);
 
@@ -180,6 +253,35 @@ impl<'a> ReplyData<'a> {
             }
         }
     }
+
+    fn emit_cw_reply_on(&self) -> TokenStream {
+        let sylvia = crate_module();
+        let is_always = self
+            .handlers
+            .iter()
+            .any(|(_, reply_on)| reply_on == &ReplyOn::Always);
+        let is_success = self
+            .handlers
+            .iter()
+            .any(|(_, reply_on)| reply_on == &ReplyOn::Success);
+        let is_failure = self
+            .handlers
+            .iter()
+            .any(|(_, reply_on)| reply_on == &ReplyOn::Failure);
+
+        if is_always || (is_success && is_failure) {
+            quote! { #sylvia ::cw_std::ReplyOn::Always }
+        } else if is_success {
+            quote! { #sylvia ::cw_std::ReplyOn::Success }
+        } else if is_failure {
+            quote! { #sylvia ::cw_std::ReplyOn::Error }
+        } else {
+            // This should never happen.
+            // We parse only the `Success`, `Failure` and `Always` values which are covered above.
+            // Handling the `Never` value wouldn't make sense as we would create a dead handler.
+            quote! { #sylvia ::cw_std::ReplyOn::Never }
+        }
+    }
 }
 
 /// Emits match arm for [ReplyOn::Success].
@@ -255,7 +357,7 @@ fn emit_failure_match_arm(
 
 trait ReplyVariant<'a> {
     fn as_handlers(&'a self) -> Vec<&'a Ident>;
-    fn as_reply_data(&self) -> Vec<(Ident, &Ident, ReplyOn)>;
+    fn as_reply_data(&self) -> Vec<(Ident, &Ident, &Ident, ReplyOn)>;
 }
 
 impl<'a> ReplyVariant<'a> for MsgVariant<'a> {
@@ -266,12 +368,13 @@ impl<'a> ReplyVariant<'a> for MsgVariant<'a> {
         self.msg_attr().handlers().iter().collect()
     }
 
-    fn as_reply_data(&self) -> Vec<(Ident, &Ident, ReplyOn)> {
+    fn as_reply_data(&self) -> Vec<(Ident, &Ident, &Ident, ReplyOn)> {
         self.as_handlers()
             .iter()
-            .map(|handler| {
+            .map(|&handler| {
                 (
                     handler.as_reply_id(),
+                    handler,
                     self.function_name(),
                     self.msg_attr().reply_on(),
                 )
diff --git a/sylvia-derive/src/contract/mt.rs b/sylvia-derive/src/contract/mt.rs
index 2da8aa1..4593419 100644
--- a/sylvia-derive/src/contract/mt.rs
+++ b/sylvia-derive/src/contract/mt.rs
@@ -10,17 +10,7 @@ use crate::parser::{
     Custom, FilteredOverrideEntryPoints, OverrideEntryPoint, ParsedSylviaAttributes,
 };
 use crate::types::msg_variant::{MsgVariant, MsgVariants};
-use crate::utils::{emit_bracketed_generics, SvCasing};
-
-fn get_ident_from_type(contract_name: &Type) -> &Ident {
-    let Type::Path(type_path) = contract_name else {
-        unreachable!()
-    };
-    let segments = &type_path.path.segments;
-    assert!(!segments.is_empty());
-    let segment = &segments.last().unwrap();
-    &segment.ident
-}
+use crate::utils::{emit_bracketed_generics, get_ident_from_type, SvCasing};
 
 /// Emits helpers for testing contract messages using MultiTest.
 ///
diff --git a/sylvia-derive/src/utils.rs b/sylvia-derive/src/utils.rs
index adf5c6f..5dff5fe 100644
--- a/sylvia-derive/src/utils.rs
+++ b/sylvia-derive/src/utils.rs
@@ -2,13 +2,15 @@ use convert_case::Casing;
 use proc_macro2::TokenStream;
 use proc_macro_error::emit_error;
 use quote::{quote, ToTokens};
+use syn::fold::Fold;
 use syn::spanned::Spanned;
 use syn::visit::Visit;
 use syn::{
-    parse_quote, GenericArgument, Ident, Path, PathArguments, ReturnType, Type, WhereClause,
-    WherePredicate,
+    parse_quote, GenericArgument, GenericParam, Ident, Path, PathArguments, ReturnType, Type,
+    WhereClause, WherePredicate,
 };
 
+use crate::fold::StripGenerics;
 use crate::parser::check_generics::{CheckGenerics, GetPath};
 
 /// Filters where predicates leaving only ones found in `used_generics`
@@ -87,6 +89,25 @@ pub fn emit_bracketed_generics<GenericT: ToTokens>(unbonded_generics: &[GenericT
     }
 }
 
+pub fn get_ident_from_type(contract_name: &Type) -> &Ident {
+    let Type::Path(type_path) = contract_name else {
+        unreachable!()
+    };
+    let segments = &type_path.path.segments;
+    assert!(!segments.is_empty());
+    let segment = &segments.last().unwrap();
+    &segment.ident
+}
+
+pub fn emit_turbofish(ty: &Type, generics: &[&GenericParam]) -> Type {
+    if !generics.is_empty() {
+        let stripped_ty = StripGenerics.fold_type(ty.clone());
+        parse_quote! { #stripped_ty :: < #(#generics),* > }
+    } else {
+        parse_quote! { #ty }
+    }
+}
+
 /// Trait for converting `Ident` to different cases preserving original [proc_macro2::Span].
 pub trait SvCasing {
     fn to_case(&self, case: convert_case::Case) -> Self;
diff --git a/sylvia/Cargo.toml b/sylvia/Cargo.toml
index fa4cf17..32bad4d 100644
--- a/sylvia/Cargo.toml
+++ b/sylvia/Cargo.toml
@@ -38,7 +38,7 @@ cosmwasm_2_0 = [
     "cw-multi-test/cosmwasm_2_0",
     "cosmwasm_1_4",
 ]
-# Enables better repli
+# Enables better replies
 sv_replies = ["sylvia-derive/sv_replies"]
 
 [dependencies]
diff --git a/sylvia/src/builder/instantiate.rs b/sylvia/src/builder/instantiate.rs
new file mode 100644
index 0000000..76115d2
--- /dev/null
+++ b/sylvia/src/builder/instantiate.rs
@@ -0,0 +1,71 @@
+use cosmwasm_std::{Binary, Coin, WasmMsg};
+
+/// Builder for the CW instantiate message.
+pub struct InstantiateBuilder {
+    /// Serialized instantiate message.
+    msg: Binary,
+    /// Id of the contract stored on the chain.
+    code_id: u64,
+    /// Admin address of the contract.
+    admin: Option<String>,
+    /// A human-readable label for the contract.
+    label: Option<String>,
+    /// Funds sent to the contract.
+    funds: Vec<Coin>,
+}
+
+impl InstantiateBuilder {
+    /// Create a new [InstantiateBuilder] with the given message and code id.
+    /// The message should be a serialized instance of the `InstantiateMsg` struct of the contract.
+    pub fn new(msg: Binary, code_id: u64) -> Self {
+        Self {
+            msg,
+            code_id,
+            admin: None,
+            label: None,
+            funds: vec![],
+        }
+    }
+
+    /// Set label for the contract.
+    pub fn with_label(mut self, label: impl Into<String>) -> Self {
+        self.label = Some(label.into());
+        self
+    }
+
+    /// Set admin address for the contract.
+    pub fn with_admin(mut self, admin: String) -> Self {
+        self.admin = Some(admin);
+        self
+    }
+
+    /// Set funds to be sent to the contract.
+    pub fn with_funds(mut self, funds: Vec<Coin>) -> Self {
+        self.funds = funds;
+        self
+    }
+
+    /// Build [WasmMsg::Instantiate] message.
+    pub fn build(self) -> WasmMsg {
+        WasmMsg::Instantiate {
+            code_id: self.code_id,
+            msg: self.msg,
+            admin: self.admin,
+            label: self.label.unwrap_or_default(),
+            funds: self.funds,
+        }
+    }
+
+    #[cfg(feature = "cosmwasm_1_2")]
+    /// Build [WasmMsg::Instantiate2] message.
+    pub fn build2(self, salt: Binary) -> WasmMsg {
+        WasmMsg::Instantiate2 {
+            code_id: self.code_id,
+            msg: self.msg,
+            admin: self.admin,
+            label: self.label.unwrap_or_default(),
+            funds: self.funds,
+            salt,
+        }
+    }
+}
diff --git a/sylvia/src/builder/mod.rs b/sylvia/src/builder/mod.rs
new file mode 100644
index 0000000..fef8bbf
--- /dev/null
+++ b/sylvia/src/builder/mod.rs
@@ -0,0 +1,3 @@
+//! This module contains builder types.
+
+pub mod instantiate;
diff --git a/sylvia/src/lib.rs b/sylvia/src/lib.rs
index 1945c6f..df65090 100644
--- a/sylvia/src/lib.rs
+++ b/sylvia/src/lib.rs
@@ -3,6 +3,7 @@
 //!
 //! Most of implementation lies in `sylvia-derive` crate which is reexported here
 
+pub mod builder;
 pub mod into_response;
 #[cfg_attr(docsrs, doc(cfg(feature = "mt")))]
 #[cfg(feature = "mt")]


Test patch:
diff --git a/sylvia/tests/reply.rs b/sylvia/tests/reply.rs
index 25d7778..9cc0e13 100644
--- a/sylvia/tests/reply.rs
+++ b/sylvia/tests/reply.rs
@@ -1,39 +1,52 @@
 #![cfg(feature = "sv_replies")]
 
-use cosmwasm_std::SubMsgResult;
+use cosmwasm_std::{BankMsg, CosmosMsg, Empty, SubMsgResult};
 use cw_storage_plus::Item;
 use cw_utils::{parse_instantiate_response_data, ParseReplyError};
-use noop_contract::sv::Executor;
-use sv::{ALWAYS_REPLY_ID, FAILURE_REPLY_ID, REMOTE_INSTANTIATED_REPLY_ID, SUCCESS_REPLY_ID};
-use sylvia::cw_std::{to_json_binary, Addr, Binary, ReplyOn, Response, StdError, SubMsg, WasmMsg};
-use sylvia::types::{ExecCtx, InstantiateCtx, QueryCtx, Remote, ReplyCtx};
+use noop_contract::sv::{Executor, NoopContractInstantiateBuilder};
+use sv::{
+    SubMsgMethods, ALWAYS_REPLY_ID, FAILURE_REPLY_ID, REMOTE_INSTANTIATED_REPLY_ID,
+    SUCCESS_REPLY_ID,
+};
+use sylvia::builder::instantiate::InstantiateBuilder;
+use sylvia::cw_std::{Addr, Binary, Response, StdError, SubMsg};
+use sylvia::types::{CustomMsg, CustomQuery, ExecCtx, InstantiateCtx, QueryCtx, Remote, ReplyCtx};
 use sylvia::{contract, entry_points};
 use thiserror::Error;
 
 #[allow(dead_code)]
 mod noop_contract {
-    use cosmwasm_std::{StdError, StdResult};
-    use sylvia::types::{ExecCtx, InstantiateCtx};
+    use cosmwasm_std::{Empty, StdError, StdResult};
+    use sylvia::types::{CustomMsg, CustomQuery, ExecCtx, InstantiateCtx};
     use sylvia::{contract, entry_points};
 
     use sylvia::cw_std::Response;
 
-    pub struct NoopContract;
+    pub struct NoopContract<M, Q> {
+        _phantom: std::marker::PhantomData<(M, Q)>,
+    }
 
-    #[entry_points]
+    #[entry_points(generics<Empty, Empty>)]
     #[contract]
-    impl NoopContract {
+    #[sv::custom(msg=M, query=Q)]
+    impl<M, Q> NoopContract<M, Q>
+    where
+        M: CustomMsg + 'static,
+        Q: CustomQuery + 'static,
+    {
         pub const fn new() -> Self {
-            Self
+            Self {
+                _phantom: std::marker::PhantomData,
+            }
         }
 
         #[sv::msg(instantiate)]
-        fn instantiate(&self, _ctx: InstantiateCtx) -> StdResult<Response> {
+        fn instantiate(&self, _ctx: InstantiateCtx<Q>) -> StdResult<Response<M>> {
             Ok(Response::new())
         }
 
         #[sv::msg(exec)]
-        fn noop(&self, _ctx: ExecCtx, should_fail: bool) -> StdResult<Response> {
+        fn noop(&self, _ctx: ExecCtx<Q>, should_fail: bool) -> StdResult<Response<M>> {
             if should_fail {
                 Err(StdError::generic_err("Failed as requested"))
             } else {
@@ -52,72 +65,141 @@ pub enum ContractError {
     ParseReply(#[from] ParseReplyError),
 }
 
-pub struct Contract {
-    remote: Item<Remote<'static, noop_contract::NoopContract>>,
+pub struct Contract<M, Q> {
+    remote: Item<Remote<'static, noop_contract::NoopContract<Empty, Empty>>>,
     last_reply: Item<u64>,
+    _phantom: std::marker::PhantomData<(M, Q)>,
 }
 
-#[entry_points]
+#[entry_points(generics<Empty, Empty>)]
 #[contract]
 #[sv::error(ContractError)]
-impl Contract {
+#[sv::custom(msg=M, query=Q)]
+impl<M, Q> Contract<M, Q>
+where
+    M: CustomMsg + 'static,
+    Q: CustomQuery + 'static,
+{
     pub fn new() -> Self {
         Self {
             remote: Item::new("remote"),
             last_reply: Item::new("last_reply"),
+            _phantom: std::marker::PhantomData,
         }
     }
 
     #[sv::msg(instantiate)]
     pub fn instantiate(
         &self,
-        _ctx: InstantiateCtx,
+        _ctx: InstantiateCtx<Q>,
         remote_code_id: u64,
-    ) -> Result<Response, ContractError> {
-        let msg = WasmMsg::Instantiate {
-            admin: None,
-            code_id: remote_code_id,
-            msg: to_json_binary(&noop_contract::sv::InstantiateMsg::new())?,
-            funds: vec![],
-            label: "noop".to_string(),
-        };
-        let sub_msg = SubMsg::reply_on_success(msg, REMOTE_INSTANTIATED_REPLY_ID);
+    ) -> Result<Response<M>, ContractError> {
+        let sub_msg = InstantiateBuilder::noop_contract(remote_code_id)?
+            .with_label("noop")
+            .build()
+            .remote_instantiated();
         Ok(Response::new().add_submessage(sub_msg))
     }
 
     #[sv::msg(exec)]
-    pub fn call_remote(
+    pub fn call_remote_success(
         &self,
-        ctx: ExecCtx,
-        reply_id: u64,
+        ctx: ExecCtx<Q>,
+        should_fail: bool,
+    ) -> Result<Response<M>, ContractError> {
+        let msg = self
+            .remote
+            .load(ctx.deps.storage)?
+            .executor()
+            .noop(should_fail)?
+            .build()
+            .success();
+
+        Ok(Response::new().add_submessage(msg))
+    }
+
+    #[sv::msg(exec)]
+    pub fn call_remote_failure(
+        &self,
+        ctx: ExecCtx<Q>,
+        should_fail: bool,
+    ) -> Result<Response<M>, ContractError> {
+        let msg = self
+            .remote
+            .load(ctx.deps.storage)?
+            .executor()
+            .noop(should_fail)?
+            .build()
+            .failure();
+
+        Ok(Response::new().add_submessage(msg))
+    }
+
+    #[sv::msg(exec)]
+    pub fn call_remote_both(
+        &self,
+        ctx: ExecCtx<Q>,
+        should_fail: bool,
+    ) -> Result<Response<M>, ContractError> {
+        let msg = self
+            .remote
+            .load(ctx.deps.storage)?
+            .executor()
+            .noop(should_fail)?
+            .build()
+            .both();
+
+        Ok(Response::new().add_submessage(msg))
+    }
+
+    #[sv::msg(exec)]
+    pub fn call_remote_always(
+        &self,
+        ctx: ExecCtx<Q>,
+        should_fail: bool,
+    ) -> Result<Response<M>, ContractError> {
+        let msg = self
+            .remote
+            .load(ctx.deps.storage)?
+            .executor()
+            .noop(should_fail)?
+            .build()
+            .always();
+
+        Ok(Response::new().add_submessage(msg))
+    }
+
+    #[sv::msg(exec)]
+    pub fn call_remote_unknown_id(
+        &self,
+        ctx: ExecCtx<Q>,
         should_fail: bool,
-        reply_on: ReplyOn,
-    ) -> Result<Response, ContractError> {
+        reply_id: u64,
+    ) -> Result<Response<M>, ContractError> {
         let msg = self
             .remote
             .load(ctx.deps.storage)?
             .executor()
             .noop(should_fail)?
             .build();
-        let mut msg = SubMsg::new(msg);
-        msg.id = reply_id;
-        msg.reply_on = reply_on;
+
+        let msg = SubMsg::reply_always(msg, reply_id);
 
         Ok(Response::new().add_submessage(msg))
     }
 
     #[sv::msg(query)]
-    pub fn last_reply(&self, ctx: QueryCtx) -> Result<u64, ContractError> {
+    pub fn last_reply(&self, ctx: QueryCtx<Q>) -> Result<u64, ContractError> {
         self.last_reply.load(ctx.deps.storage).map_err(Into::into)
     }
 
     #[sv::msg(reply, reply_on=success)]
     fn remote_instantiated(
         &self,
-        ctx: ReplyCtx,
+        ctx: ReplyCtx<Q>,
         data: Option<Binary>,
         _payload: Binary,
-    ) -> Result<Response, ContractError> {
+    ) -> Result<Response<M>, ContractError> {
         self.last_reply
             .save(ctx.deps.storage, &REMOTE_INSTANTIATED_REPLY_ID)?;
         let init_data = parse_instantiate_response_data(&data.unwrap())?;
@@ -132,10 +214,10 @@ impl Contract {
     #[sv::msg(reply, handlers=[success, both], reply_on=success)]
     fn success(
         &self,
-        ctx: ReplyCtx,
+        ctx: ReplyCtx<Q>,
         _data: Option<Binary>,
         _payload: Binary,
-    ) -> Result<Response, ContractError> {
+    ) -> Result<Response<M>, ContractError> {
         self.last_reply.save(ctx.deps.storage, &SUCCESS_REPLY_ID)?;
 
         Ok(Response::new())
@@ -144,10 +226,10 @@ impl Contract {
     #[sv::msg(reply, handlers=[failure, both], reply_on=failure)]
     fn failure(
         &self,
-        ctx: ReplyCtx,
+        ctx: ReplyCtx<Q>,
         _error: String,
         _payload: Binary,
-    ) -> Result<Response, ContractError> {
+    ) -> Result<Response<M>, ContractError> {
         self.last_reply.save(ctx.deps.storage, &FAILURE_REPLY_ID)?;
 
         Ok(Response::new())
@@ -156,26 +238,36 @@ impl Contract {
     #[sv::msg(reply, reply_on=always)]
     fn always(
         &self,
-        ctx: ReplyCtx,
+        ctx: ReplyCtx<Q>,
         _result: SubMsgResult,
         _payload: Binary,
-    ) -> Result<Response, ContractError> {
+    ) -> Result<Response<M>, ContractError> {
         self.last_reply.save(ctx.deps.storage, &ALWAYS_REPLY_ID)?;
 
         Ok(Response::new())
     }
+
+    #[sv::msg(exec)]
+    fn send_cosmos_messages(&self, ctx: ExecCtx<Q>) -> Result<Response<M>, ContractError> {
+        let remote_addr = self.remote.load(ctx.deps.storage)?;
+        let cosmos_msg = CosmosMsg::Bank(BankMsg::Send {
+            to_address: remote_addr.as_ref().to_string(),
+            amount: vec![],
+        });
+        let submsg = cosmos_msg.always();
+        Ok(Response::new().add_submessage(submsg))
+    }
 }
 
 mod tests {
     use crate::noop_contract::sv::mt::CodeId as NoopCodeId;
     use crate::sv::mt::{CodeId, ContractProxy};
     use crate::sv::{
-        ALWAYS_REPLY_ID, BOTH_REPLY_ID, FAILURE_REPLY_ID, REMOTE_INSTANTIATED_REPLY_ID,
-        SUCCESS_REPLY_ID,
+        ALWAYS_REPLY_ID, FAILURE_REPLY_ID, REMOTE_INSTANTIATED_REPLY_ID, SUCCESS_REPLY_ID,
     };
 
     use sylvia::cw_multi_test::IntoBech32;
-    use sylvia::cw_std::{ReplyOn, StdError};
+    use sylvia::cw_std::StdError;
     use sylvia::multitest::App;
 
     #[test]
@@ -196,74 +288,55 @@ mod tests {
         assert_eq!(last_reply, REMOTE_INSTANTIATED_REPLY_ID);
 
         // Should not dispatch if expected success and execution failed
-        let err = contract
-            .call_remote(SUCCESS_REPLY_ID, true, ReplyOn::Success)
-            .call(&owner)
-            .unwrap_err();
+        let err = contract.call_remote_success(true).call(&owner).unwrap_err();
         assert_eq!(err, StdError::generic_err("Failed as requested").into());
         let last_reply = contract.last_reply().unwrap();
         assert_eq!(last_reply, REMOTE_INSTANTIATED_REPLY_ID);
 
         // Should dispatch if expected success and execution succeeded
-        contract
-            .call_remote(SUCCESS_REPLY_ID, false, ReplyOn::Success)
-            .call(&owner)
-            .unwrap();
+        contract.call_remote_success(false).call(&owner).unwrap();
         let last_reply = contract.last_reply().unwrap();
         assert_eq!(last_reply, SUCCESS_REPLY_ID);
 
         // Should not dispatch if expected failure and execution succeeded
-        contract
-            .call_remote(FAILURE_REPLY_ID, false, ReplyOn::Error)
-            .call(&owner)
-            .unwrap();
+        contract.call_remote_failure(false).call(&owner).unwrap();
         let last_reply = contract.last_reply().unwrap();
         assert_eq!(last_reply, SUCCESS_REPLY_ID);
 
         // Should dispatch if expected failure and execution failed
-        contract
-            .call_remote(FAILURE_REPLY_ID, true, ReplyOn::Error)
-            .call(&owner)
-            .unwrap();
+        contract.call_remote_failure(true).call(&owner).unwrap();
         let last_reply = contract.last_reply().unwrap();
         assert_eq!(last_reply, FAILURE_REPLY_ID);
 
         // Should dispatch if expected any result and execution succeeded
-        contract
-            .call_remote(ALWAYS_REPLY_ID, false, ReplyOn::Always)
-            .call(&owner)
-            .unwrap();
-        let last_reply = contract.last_reply().unwrap();
-        assert_eq!(last_reply, ALWAYS_REPLY_ID);
-
-        // Should dispatch if expected any result and execution failed
-        contract
-            .call_remote(ALWAYS_REPLY_ID, true, ReplyOn::Always)
-            .call(&owner)
-            .unwrap();
+        contract.call_remote_always(false).call(&owner).unwrap();
         let last_reply = contract.last_reply().unwrap();
         assert_eq!(last_reply, ALWAYS_REPLY_ID);
 
         // Should dispatch if expected both results and execution succeeded
-        contract
-            .call_remote(BOTH_REPLY_ID, false, ReplyOn::Success)
-            .call(&owner)
-            .unwrap();
+        contract.call_remote_both(false).call(&owner).unwrap();
         let last_reply = contract.last_reply().unwrap();
         assert_eq!(last_reply, SUCCESS_REPLY_ID);
 
+        // Should dispatch if expected any result and execution failed
+        contract.call_remote_always(true).call(&owner).unwrap();
+        let last_reply = contract.last_reply().unwrap();
+        assert_eq!(last_reply, ALWAYS_REPLY_ID);
+
         // Should dispatch if expected both results and execution failed
-        contract
-            .call_remote(BOTH_REPLY_ID, true, ReplyOn::Error)
-            .call(&owner)
-            .unwrap();
+        contract.call_remote_both(true).call(&owner).unwrap();
         let last_reply = contract.last_reply().unwrap();
         assert_eq!(last_reply, FAILURE_REPLY_ID);
 
+        // Should send the cosmos message
+        contract.send_cosmos_messages().call(&owner).unwrap();
+        let last_reply = contract.last_reply().unwrap();
+        assert_eq!(last_reply, ALWAYS_REPLY_ID);
+
         // Should return error if unknown reply ID received
         let unknown_reply_id = 42u64;
         let err = contract
-            .call_remote(unknown_reply_id, false, ReplyOn::Success)
+            .call_remote_unknown_id(false, unknown_reply_id)
             .call(&owner)
             .unwrap_err();
         assert_eq!(