cosm_script/
contract.rs

1use std::{
2    env,
3    str::{from_utf8, FromStr},
4    time::Duration,
5};
6
7use cosmrs::{
8    cosmwasm::{MsgExecuteContract, MsgInstantiateContract},
9    AccountId, Coin,
10};
11
12use serde::{de::DeserializeOwned, Serialize};
13use serde_json::from_str;
14
15use crate::{
16    cosmos_modules, data_structures::network::NetworkKind, error::CosmScriptError,
17    multisig::Multisig, sender::Wallet, CosmTxResponse, Deployment,
18};
19
20pub struct ContractInstance<'a> {
21    pub deployment: &'a Deployment,
22    pub name: &'a str,
23    pub sender: Wallet<'a>,
24    /// Allows for setting a custom code-id in the Instance trait implemtation
25    code_id_key: Option<&'a str>,
26}
27
28impl<'a> ContractInstance<'a> {
29    pub fn new(
30        name: &'a str,
31        sender: Wallet<'a>,
32        deployment: &'a Deployment,
33    ) -> anyhow::Result<Self> {
34        let instance = ContractInstance {
35            deployment,
36            name,
37            sender,
38            code_id_key: None,
39        };
40        Ok(instance)
41    }
42
43    /// Used to overwrite the code-id getter key. Useful when you want shared code between multiple contract instances
44    /// Example: Two CW20 tokens that use the same code-id but have a different name. see
45    pub fn overwrite_code_id_key(&mut self, code_id_key_to_use: &'static str) {
46        self.code_id_key = Some(code_id_key_to_use);
47    }
48
49    pub async fn execute<E: Serialize>(
50        &self,
51        exec_msg: &E,
52        coins: &[Coin],
53    ) -> Result<CosmTxResponse, CosmScriptError> {
54        let contract = self.get_address()?;
55        log::info!("executing on {} at {}", self.name, contract);
56
57        let exec_msg: MsgExecuteContract = if self.deployment.proposal {
58            Multisig::create_proposal(
59                &exec_msg,
60                &self.deployment.name,
61                &contract,
62                &env::var(&self.deployment.network.kind.multisig_name())?,
63                self.sender.pub_addr()?,
64                coins,
65            )?
66        } else {
67            MsgExecuteContract {
68                sender: self.sender.pub_addr()?,
69                contract: AccountId::from_str(&self.get_address()?)?,
70                msg: serde_json::to_vec(&exec_msg)?,
71                funds: coins.to_vec(),
72            }
73        };
74
75        let result = self.sender.commit_tx(vec![exec_msg], None).await?;
76
77        Ok(result)
78    }
79
80    pub async fn instantiate<I: Serialize>(
81        &self,
82        init_msg: I,
83        admin: Option<String>,
84        coins: &[Coin],
85    ) -> Result<CosmTxResponse, CosmScriptError> {
86        let sender = self.sender;
87        let key = self.code_id_key.unwrap_or(self.name);
88        let code_id = self.deployment.network.get_latest_version(key)?;
89
90        let memo = format!("Contract: {}, Group: {}", self.name, self.deployment.name);
91
92        log::info!("instantiating {}", self.name);
93
94        let init_msg = MsgInstantiateContract {
95            code_id,
96            label: Some(self.name.into()),
97            admin: admin.map(|a| FromStr::from_str(&a).unwrap()),
98            sender: sender.pub_addr()?,
99            msg: serde_json::to_vec(&init_msg)?,
100            funds: coins.to_vec(),
101        };
102
103        let result = sender.commit_tx(vec![init_msg], Some(&memo)).await?;
104        let address = &result.get_attribute_from_logs("instantiate", "_contract_address")[0].1;
105
106        log::debug!("{} address: {:?}", self.name, address);
107        self.save_contract_address(address)?;
108
109        Ok(result)
110    }
111
112    pub async fn query<Q: Serialize, T: Serialize + DeserializeOwned>(
113        &self,
114        query_msg: Q,
115    ) -> Result<T, CosmScriptError> {
116        let sender = self.sender;
117
118        let mut client = cosmos_modules::cosmwasm::query_client::QueryClient::new(sender.channel());
119        let resp = client
120            .smart_contract_state(cosmos_modules::cosmwasm::QuerySmartContractStateRequest {
121                address: self.get_address()?,
122                query_data: serde_json::to_vec(&query_msg)?,
123            })
124            .await?;
125
126        Ok(from_str(from_utf8(&resp.into_inner().data).unwrap())?)
127    }
128
129    /// Uploads given .wasm file and stores resulting code-id in contract store.
130    /// *path* can be either a full/relative path. (indicated by the .wasm) or just a regular name. In the second case the WASM_DIR env var
131    /// will be read and the path will be costructed to be WASM_DIR/*path*.wasm
132    pub async fn upload(&self, path: &str) -> Result<CosmTxResponse, CosmScriptError> {
133        let sender = &self.sender;
134        let memo = format!("Contract: {}, Group: {}", self.name, self.deployment.name);
135        let wasm_path = if path.contains(".wasm") {
136            path.to_string()
137        } else {
138            format!("{}/{}.wasm", env::var("WASM_DIR").unwrap(), path)
139        };
140
141        log::debug!("{}", wasm_path);
142
143        let file_contents = std::fs::read(wasm_path)?;
144        let store_msg = cosmrs::cosmwasm::MsgStoreCode {
145            sender: sender.pub_addr()?,
146            wasm_byte_code: file_contents,
147            instantiate_permission: None,
148        };
149        let result = sender.commit_tx(vec![store_msg], Some(&memo)).await?;
150
151        log::info!("uploaded: {:?}", result.txhash);
152        // TODO: check why logs are empty
153
154        let code_id = result.get_attribute_from_logs("store_code", "code_id")[0]
155            .1
156            .parse::<u64>()?;
157        log::info!("code_id: {:?}", code_id);
158        self.save_code_id(code_id)?;
159        wait(self.deployment).await;
160        Ok(result)
161    }
162
163    pub async fn migrate<M: Serialize>(
164        &self,
165        _migrate_msg: M,
166        _new_code_id: u64,
167    ) -> Result<CosmTxResponse, CosmScriptError> {
168        todo!()
169
170        // let sender = &self.sender;
171        // let migrate_msg_json = json!(migrate_msg);
172
173        // let wasm = Wasm::create(&sender.terra);
174
175        // let old_code_id = wasm.info(&self.get_address()?).await?.result.code_id;
176        // let memo = format!("Contract: {}, OldCodeId: {}", self.name, old_code_id);
177
178        // let resp = wasm
179        //     .migrate(
180        //         &sender.secp,
181        //         &sender.private_key,
182        //         &self.get_address()?,
183        //         new_code_id,
184        //         Some(migrate_msg_json.to_string()),
185        //         Some(memo),
186        //     )
187        //     .await?;
188
189        // let result = sender
190        //     .terra
191        //     .tx()
192        //     .get_and_wait_v1(&resp.txhash, 15, Duration::from_secs(2))
193        //     .await?;
194
195        // wait(self.deployment).await;
196        // Ok(result)
197    }
198
199    // Getters //
200
201    pub fn get_address(&self) -> Result<String, CosmScriptError> {
202        self.deployment.get_contract_address(self.name)
203    }
204
205    /// get the on-chain contract code-id
206    pub async fn get_code_id(&self) -> Result<u64, CosmScriptError> {
207        let addr = self.get_address()?;
208        let channel = self.deployment.network.grpc_channel.clone();
209        let mut client = cosmos_modules::cosmwasm::query_client::QueryClient::new(channel);
210
211        let resp = client
212            .contract_info(cosmos_modules::cosmwasm::QueryContractInfoRequest { address: addr })
213            .await?
214            .into_inner();
215
216        let code_id = resp.contract_info.unwrap().code_id;
217        Ok(code_id)
218    }
219
220    // Setters //
221
222    pub fn save_code_id(&self, code_id: u64) -> Result<(), CosmScriptError> {
223        self.deployment
224            .network
225            .set_contract_version(self.name, code_id)
226    }
227
228    pub fn save_contract_address(&self, contract_address: &str) -> Result<(), CosmScriptError> {
229        self.deployment
230            .save_contract_address(self.name, contract_address)
231    }
232
233    pub async fn is_local_version(&self) -> anyhow::Result<bool> {
234        todo!()
235
236        // let on_chain_encoded_hash = self
237        //     .sender
238        //     .terra
239        //     .wasm()
240        //     .codes(self.get_code_id()?)
241        //     .await?
242        //     .result
243        //     .code_hash;
244        // let path = format!("{}/checksums.txt", env::var("WASM_DIR")?);
245
246        // let contents = fs::read_to_string(path).expect("Something went wrong reading the file");
247
248        // let parsed: Vec<&str> = contents.rsplit(".wasm").collect();
249
250        // let name = self.name.split(':').last().unwrap();
251
252        // let containing_line = parsed
253        //     .iter()
254        //     .filter(|line| line.contains(name))
255        //     .next()
256        //     .unwrap();
257        // log::debug!("{:?}", containing_line);
258
259        // let local_hash = containing_line
260        //     .trim_start_matches('\n')
261        //     .split_whitespace()
262        //     .next()
263        //     .unwrap();
264
265        // let on_chain_hash = base16::encode_lower(&decode(on_chain_encoded_hash)?);
266        // Ok(on_chain_hash == local_hash)
267    }
268}
269
270async fn wait(deployment: &Deployment) {
271    match deployment.network.kind {
272        NetworkKind::Local => tokio::time::sleep(Duration::from_secs(6)).await,
273        NetworkKind::Mainnet => tokio::time::sleep(Duration::from_secs(60)).await,
274        NetworkKind::Testnet => tokio::time::sleep(Duration::from_secs(30)).await,
275    }
276}