abstract_sdk/base/
contract_base.rs

1use abstract_std::ibc::{Callback, IbcResult, ModuleIbcInfo};
2use cosmwasm_std::{Binary, Deps, DepsMut, Env, MessageInfo, Reply, Response, Storage};
3use cw2::{ContractVersion, CONTRACT};
4
5use super::handler::Handler;
6use crate::{std::objects::dependency::StaticDependency, AbstractSdkError, AbstractSdkResult};
7
8/// ID of the module.
9pub type ModuleId = &'static str;
10/// Version of the contract in str format.
11pub type VersionString = &'static str;
12/// Metadata of the module in str format.
13pub type ModuleMetadata = Option<&'static str>;
14
15// ANCHOR: init
16/// Function signature for an instantiate handler.
17pub type InstantiateHandlerFn<Module, CustomInitMsg, Error> =
18    fn(DepsMut, Env, MessageInfo, Module, CustomInitMsg) -> Result<Response, Error>;
19// ANCHOR_END: init
20
21// ANCHOR: exec
22/// Function signature for an execute handler.
23pub type ExecuteHandlerFn<Module, CustomExecMsg, Error> =
24    fn(DepsMut, Env, MessageInfo, Module, CustomExecMsg) -> Result<Response, Error>;
25// ANCHOR_END: exec
26
27// ANCHOR: query
28/// Function signature for a query handler.
29pub type QueryHandlerFn<Module, CustomQueryMsg, Error> =
30    fn(Deps, Env, &Module, CustomQueryMsg) -> Result<Binary, Error>;
31// ANCHOR_END: query
32
33// ANCHOR: ibc
34/// Function signature for an IBC callback handler.
35pub type IbcCallbackHandlerFn<Module, Error> =
36    fn(DepsMut, Env, Module, Callback, IbcResult) -> Result<Response, Error>;
37// ANCHOR_END: ibc
38
39// ANCHOR: module_ibc
40/// Function signature for an Module to Module IBC handler.
41pub type ModuleIbcHandlerFn<Module, Error> =
42    fn(DepsMut, Env, Module, ModuleIbcInfo, Binary) -> Result<Response, Error>;
43// ANCHOR_END: module_ibc
44
45// ANCHOR: mig
46/// Function signature for a migrate handler.
47pub type MigrateHandlerFn<Module, CustomMigrateMsg, Error> =
48    fn(DepsMut, Env, Module, CustomMigrateMsg) -> Result<Response, Error>;
49// ANCHOR_END: mig
50
51// ANCHOR: sudo
52/// Function signature for a sudo handler.
53pub type SudoHandlerFn<Module, CustomSudoMsg, Error> =
54    fn(DepsMut, Env, Module, CustomSudoMsg) -> Result<Response, Error>;
55// ANCHOR_END: sudo
56
57// ANCHOR: reply
58/// Function signature for a reply handler.
59pub type ReplyHandlerFn<Module, Error> = fn(DepsMut, Env, Module, Reply) -> Result<Response, Error>;
60// ANCHOR_END: reply
61
62/// There can be two locations where reply handlers are added.
63/// 1. Base implementation of the contract.
64/// 2. Custom implementation of the contract.
65const MAX_REPLY_COUNT: usize = 2;
66
67/// Abstract generic contract
68pub struct AbstractContract<Module: Handler + 'static, Error: From<AbstractSdkError> + 'static> {
69    /// Static info about the contract, used for migration
70    pub(crate) info: (ModuleId, VersionString, ModuleMetadata),
71    /// Modules that this contract depends on.
72    pub(crate) dependencies: &'static [StaticDependency],
73    /// Handler of instantiate messages.
74    pub(crate) instantiate_handler:
75        Option<InstantiateHandlerFn<Module, <Module as Handler>::CustomInitMsg, Error>>,
76    /// Handler of execute messages.
77    pub(crate) execute_handler:
78        Option<ExecuteHandlerFn<Module, <Module as Handler>::CustomExecMsg, Error>>,
79    /// Handler of query messages.
80    pub(crate) query_handler:
81        Option<QueryHandlerFn<Module, <Module as Handler>::CustomQueryMsg, Error>>,
82    /// Handler for migrations.
83    pub(crate) migrate_handler:
84        Option<MigrateHandlerFn<Module, <Module as Handler>::CustomMigrateMsg, Error>>,
85    /// Handler for sudo messages.
86    pub(crate) sudo_handler: Option<SudoHandlerFn<Module, <Module as Handler>::SudoMsg, Error>>,
87    /// List of reply handlers per reply ID.
88    pub reply_handlers: [&'static [(u64, ReplyHandlerFn<Module, Error>)]; MAX_REPLY_COUNT],
89    /// IBC callback handler following an IBC action
90    pub(crate) ibc_callback_handler: Option<IbcCallbackHandlerFn<Module, Error>>,
91    /// Module IBC handler for passing messages between a module on different chains.
92    pub(crate) module_ibc_handler: Option<ModuleIbcHandlerFn<Module, Error>>,
93}
94
95impl<Module, Error: From<AbstractSdkError>> AbstractContract<Module, Error>
96where
97    Module: Handler,
98{
99    /// Creates a new customizable abstract contract.
100    pub const fn new(name: ModuleId, version: VersionString, metadata: ModuleMetadata) -> Self {
101        Self {
102            info: (name, version, metadata),
103            ibc_callback_handler: None,
104            reply_handlers: [&[], &[]],
105            dependencies: &[],
106            execute_handler: None,
107            migrate_handler: None,
108            sudo_handler: None,
109            instantiate_handler: None,
110            query_handler: None,
111            module_ibc_handler: None,
112        }
113    }
114    /// Gets the cw2 version of the contract.
115    pub fn version(&self, store: &dyn Storage) -> AbstractSdkResult<ContractVersion> {
116        CONTRACT.load(store).map_err(Into::into)
117    }
118    /// Gets the static info of the contract.
119    pub fn info(&self) -> (ModuleId, VersionString, ModuleMetadata) {
120        self.info
121    }
122    /// add dependencies to the contract
123    pub const fn with_dependencies(mut self, dependencies: &'static [StaticDependency]) -> Self {
124        self.dependencies = dependencies;
125        self
126    }
127    /// Add reply handlers to the contract.
128    pub const fn with_replies(
129        mut self,
130        reply_handlers: [&'static [(u64, ReplyHandlerFn<Module, Error>)]; MAX_REPLY_COUNT],
131    ) -> Self {
132        self.reply_handlers = reply_handlers;
133        self
134    }
135
136    /// add IBC callback handler to contract
137    pub const fn with_ibc_callback(
138        mut self,
139        callback: IbcCallbackHandlerFn<Module, Error>,
140    ) -> Self {
141        self.ibc_callback_handler = Some(callback);
142        self
143    }
144
145    /// add IBC callback handler to contract
146    pub const fn with_module_ibc(
147        mut self,
148        module_handler: ModuleIbcHandlerFn<Module, Error>,
149    ) -> Self {
150        self.module_ibc_handler = Some(module_handler);
151        self
152    }
153
154    /// Add instantiate handler to the contract.
155    pub const fn with_instantiate(
156        mut self,
157        instantiate_handler: InstantiateHandlerFn<
158            Module,
159            <Module as Handler>::CustomInitMsg,
160            Error,
161        >,
162    ) -> Self {
163        self.instantiate_handler = Some(instantiate_handler);
164        self
165    }
166
167    /// Add query handler to the contract.
168    pub const fn with_migrate(
169        mut self,
170        migrate_handler: MigrateHandlerFn<Module, <Module as Handler>::CustomMigrateMsg, Error>,
171    ) -> Self {
172        self.migrate_handler = Some(migrate_handler);
173        self
174    }
175
176    /// Add sudo handler to the contract.
177    pub const fn with_sudo(
178        mut self,
179        sudo_handler: SudoHandlerFn<Module, <Module as Handler>::SudoMsg, Error>,
180    ) -> Self {
181        self.sudo_handler = Some(sudo_handler);
182        self
183    }
184
185    /// Add execute handler to the contract.
186    pub const fn with_execute(
187        mut self,
188        execute_handler: ExecuteHandlerFn<Module, <Module as Handler>::CustomExecMsg, Error>,
189    ) -> Self {
190        self.execute_handler = Some(execute_handler);
191        self
192    }
193
194    /// Add query handler to the contract.
195    pub const fn with_query(
196        mut self,
197        query_handler: QueryHandlerFn<Module, <Module as Handler>::CustomQueryMsg, Error>,
198    ) -> Self {
199        self.query_handler = Some(query_handler);
200        self
201    }
202}
203
204#[cfg(test)]
205mod test {
206    #![allow(clippy::needless_borrows_for_generic_args)]
207    use cosmwasm_std::Empty;
208
209    use super::*;
210
211    #[cosmwasm_schema::cw_serde]
212    struct MockInitMsg;
213
214    #[cosmwasm_schema::cw_serde]
215    struct MockExecMsg;
216
217    #[cosmwasm_schema::cw_serde]
218    struct MockQueryMsg;
219
220    #[cosmwasm_schema::cw_serde]
221    struct MockMigrateMsg;
222
223    #[cosmwasm_schema::cw_serde]
224    struct MockReceiveMsg;
225
226    #[cosmwasm_schema::cw_serde]
227    struct MockSudoMsg;
228
229    use thiserror::Error;
230
231    #[derive(Error, Debug, PartialEq)]
232    pub enum MockError {
233        #[error(transparent)]
234        Sdk(#[from] AbstractSdkError),
235    }
236
237    struct MockModule;
238
239    type MockAppContract = AbstractContract<MockModule, MockError>;
240
241    impl Handler for MockModule {
242        type Error = MockError;
243        type CustomInitMsg = MockInitMsg;
244        type CustomExecMsg = MockExecMsg;
245        type CustomQueryMsg = MockQueryMsg;
246        type CustomMigrateMsg = MockMigrateMsg;
247        type SudoMsg = MockSudoMsg;
248
249        fn contract(&self) -> &AbstractContract<Self, Self::Error> {
250            unimplemented!()
251        }
252    }
253
254    #[coverage_helper::test]
255    fn test_info() {
256        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default());
257        let (name, version, metadata) = contract.info();
258        assert_eq!(name, "test_contract");
259        assert_eq!(version, "0.1.0");
260        assert_eq!(metadata, ModuleMetadata::default());
261    }
262
263    #[coverage_helper::test]
264    fn test_with_empty() {
265        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
266            .with_dependencies(&[]);
267
268        assert!(contract.reply_handlers.iter().all(|x| x.is_empty()));
269
270        assert!(contract.dependencies.is_empty());
271        assert!(contract.ibc_callback_handler.is_none());
272        assert!(contract.instantiate_handler.is_none());
273        assert!(contract.execute_handler.is_none());
274        assert!(contract.query_handler.is_none());
275        assert!(contract.migrate_handler.is_none());
276    }
277
278    #[coverage_helper::test]
279    fn test_with_dependencies() {
280        const VERSION: &str = "0.1.0";
281        const DEPENDENCY: StaticDependency = StaticDependency::new("test", &[VERSION]);
282        const DEPENDENCIES: &[StaticDependency] = &[DEPENDENCY];
283
284        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
285            .with_dependencies(DEPENDENCIES);
286
287        assert_eq!(contract.dependencies[0].clone(), DEPENDENCY);
288    }
289
290    #[coverage_helper::test]
291    fn test_with_instantiate() {
292        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
293            .with_instantiate(|_, _, _, _, _| {
294                Ok(Response::default().add_attribute("test", "instantiate"))
295            });
296
297        assert!(contract.instantiate_handler.is_some());
298    }
299
300    #[coverage_helper::test]
301    fn test_with_sudo() {
302        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
303            .with_sudo(|_, _, _, _| Ok(Response::default().add_attribute("test", "sudo")));
304
305        assert!(contract.sudo_handler.is_some());
306    }
307
308    #[coverage_helper::test]
309    fn test_with_execute() {
310        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
311            .with_execute(|_, _, _, _, _| Ok(Response::default().add_attribute("test", "execute")));
312
313        assert!(contract.execute_handler.is_some());
314    }
315
316    #[coverage_helper::test]
317    fn test_with_query() {
318        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
319            .with_query(|_, _, _, _| Ok(cosmwasm_std::to_json_binary(&Empty {}).unwrap()));
320
321        assert!(contract.query_handler.is_some());
322    }
323
324    #[coverage_helper::test]
325    fn test_with_migrate() {
326        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
327            .with_migrate(|_, _, _, _| Ok(Response::default().add_attribute("test", "migrate")));
328
329        assert!(contract.migrate_handler.is_some());
330    }
331
332    #[coverage_helper::test]
333    fn test_with_reply_handlers() {
334        const REPLY_ID: u64 = 50u64;
335        const HANDLER: ReplyHandlerFn<MockModule, MockError> =
336            |_, _, _, _| Ok(Response::default().add_attribute("test", "reply"));
337        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
338            .with_replies([&[(REPLY_ID, HANDLER)], &[]]);
339
340        assert_eq!(contract.reply_handlers[0][0].0, REPLY_ID);
341        assert!(contract.reply_handlers[1].is_empty());
342    }
343
344    #[coverage_helper::test]
345    fn test_with_ibc_callback_handlers() {
346        const HANDLER: IbcCallbackHandlerFn<MockModule, MockError> =
347            |_, _, _, _, _| Ok(Response::default().add_attribute("test", "ibc"));
348        let contract = MockAppContract::new("test_contract", "0.1.0", ModuleMetadata::default())
349            .with_ibc_callback(HANDLER);
350
351        assert!(contract.ibc_callback_handler.is_some());
352    }
353}