odra_cli/
container.rs

1use std::{fs::File, io::Write, path::PathBuf, str::FromStr};
2
3use chrono::{SecondsFormat, Utc};
4use odra::{
5    contract_def::HasIdent,
6    host::{HostEnv, HostRef, HostRefLoader},
7    prelude::{Address, Addressable},
8    OdraContract
9};
10use serde_derive::{Deserialize, Serialize};
11use thiserror::Error;
12
13use crate::{
14    cmd::args::{DEPLOY_MODE_ARCHIVE, DEPLOY_MODE_OVERRIDE},
15    log,
16    utils::get_default_contracts_file
17};
18
19#[derive(Error, Debug)]
20pub enum ContractError {
21    #[error("TOML serialization error")]
22    TomlSerialize(#[from] toml::ser::Error),
23    #[error("TOML deserialization error")]
24    TomlDeserialize(#[from] toml::de::Error),
25    #[error("Couldn't read file")]
26    Io(#[from] std::io::Error),
27    #[error("Couldn't find contract `{0}`")]
28    NotFound(String),
29    #[error("Couldn't find schema file for contract `{0}`")]
30    SchemaFileNotFound(String),
31    #[error("Contract `{0}` already exists")]
32    ContractExists(String)
33}
34
35/// Represents storage for deployed contracts.
36/// This trait defines the methods for reading and writing contract data.
37pub(crate) trait ContractStorage {
38    /// Reads the contract data from the storage.
39    fn read(&self) -> Result<ContractsData, ContractError>;
40    /// Writes the contract data to the storage.
41    fn write(&mut self, data: &ContractsData) -> Result<(), ContractError>;
42    /// Creates a backup copy of the contract data.
43    fn backup(&self) -> Result<(), ContractError>;
44}
45
46/// Represents the data structure for storing deployed contracts in a TOML file.
47pub(crate) struct FileContractStorage {
48    file_path: PathBuf
49}
50
51impl FileContractStorage {
52    pub fn new(custom_path: Option<PathBuf>) -> Result<Self, ContractError> {
53        let mut path = project_root::get_project_root().map_err(ContractError::Io)?;
54        match &custom_path {
55            Some(path_str) if !path_str.to_str().unwrap_or_default().is_empty() => {
56                path.push(path_str);
57            }
58            _ => {
59                let default_file = get_default_contracts_file();
60                path.push(&default_file);
61            }
62        }
63        if !path.exists() {
64            let parent_path = path.parent().ok_or_else(|| {
65                ContractError::Io(std::io::Error::new(
66                    std::io::ErrorKind::NotFound,
67                    "Parent directory not found"
68                ))
69            })?;
70            std::fs::create_dir_all(parent_path).map_err(ContractError::Io)?;
71        }
72
73        Ok(Self { file_path: path })
74    }
75}
76
77impl ContractStorage for FileContractStorage {
78    fn read(&self) -> Result<ContractsData, ContractError> {
79        let file = std::fs::read_to_string(&self.file_path).map_err(ContractError::Io)?;
80        toml::from_str(&file).map_err(ContractError::TomlDeserialize)
81    }
82
83    fn write(&mut self, data: &ContractsData) -> Result<(), ContractError> {
84        let content = toml::to_string_pretty(&data).map_err(ContractError::TomlSerialize)?;
85        let mut file = File::create(&self.file_path).map_err(ContractError::Io)?;
86        file.write_all(content.as_bytes())
87            .map_err(ContractError::Io)?;
88        Ok(())
89    }
90
91    fn backup(&self) -> Result<(), ContractError> {
92        let mut new_path = self.file_path.with_extension("old");
93        while new_path.exists() {
94            new_path = new_path.with_added_extension("old");
95        }
96        std::fs::copy(&self.file_path, new_path).map_err(ContractError::Io)?;
97        Ok(())
98    }
99}
100
101/// This trait defines the methods for providing access to deployed contracts.
102pub trait ContractProvider {
103    /// Gets a reference to the contract.
104    ///
105    /// Returns a reference to the contract if it is found, otherwise returns an error.
106    fn contract_ref<T: OdraContract + 'static>(
107        &self,
108        env: &HostEnv
109    ) -> Result<T::HostRef, ContractError>;
110
111    /// Gets a reference to the named contract.
112    ///
113    /// Returns a reference to the contract if it is found, otherwise returns an error.
114    fn contract_ref_named<T: OdraContract + 'static>(
115        &self,
116        env: &HostEnv,
117        name: Option<String>
118    ) -> Result<T::HostRef, ContractError>;
119
120    /// Returns a list of all deployed contracts with their names and addresses.
121    fn all_contracts(&self) -> Vec<DeployedContract>;
122
123    /// Returns the contract address.
124    fn address_by_name(&self, name: &str) -> Option<Address>;
125}
126
127/// Struct representing the deployed contracts.
128///
129/// This struct is used to store the contracts name and address at the deploy
130/// time and to retrieve a reference to the contract at runtime.
131///
132/// The data is stored in a TOML file `deployed_contracts.toml` in the
133/// `{project_root}/resources` directory.
134pub struct DeployedContractsContainer {
135    data: std::cell::RefCell<ContractsData>,
136    storage: std::cell::RefCell<Box<dyn ContractStorage>>
137}
138
139impl DeployedContractsContainer {
140    /// Creates a new instance.
141    pub(crate) fn instance(storage: impl ContractStorage + 'static) -> Self {
142        match storage.read() {
143            Ok(data) => Self {
144                data: std::cell::RefCell::new(data),
145                storage: std::cell::RefCell::new(Box::new(storage))
146            },
147            Err(_) => Self {
148                data: std::cell::RefCell::new(Default::default()),
149                storage: std::cell::RefCell::new(Box::new(storage))
150            }
151        }
152    }
153
154    pub fn apply_deploy_mode(&self, mode: String) -> Result<(), ContractError> {
155        match mode.as_str() {
156            DEPLOY_MODE_OVERRIDE => {
157                self.data.borrow_mut().contracts.clear();
158                let data = self.data.borrow();
159                let mut storage = self.storage.borrow_mut();
160                storage.write(&data)?;
161                log("Contracts configuration has been overridden");
162            }
163            DEPLOY_MODE_ARCHIVE => {
164                let storage = self.storage.borrow_mut();
165                storage.backup()?;
166                self.data.borrow_mut().contracts.clear();
167                let data = self.data.borrow();
168                let mut storage = self.storage.borrow_mut();
169                storage.write(&data)?;
170                log("Starting fresh deployment. Previous contracts configuration has been backed up.");
171            }
172            _ => {} // default mode does nothing
173        }
174        Ok(())
175    }
176
177    /// Adds a contract to the container.
178    pub fn add_contract_named<T: HostRef + HasIdent>(
179        &self,
180        contract: &T,
181        package_name: Option<String>
182    ) -> Result<(), ContractError> {
183        // Try to add the contract - will fail if package_name already exists
184        self.data
185            .borrow_mut()
186            .add_contract::<T>(contract.address(), package_name)?;
187
188        // Save to storage
189        let data = self.data.borrow();
190        let mut storage = self.storage.borrow_mut();
191        storage.write(&data)
192    }
193
194    /// Adds a contract to the container.
195    pub fn add_contract<T: HostRef + HasIdent>(&self, contract: &T) -> Result<(), ContractError> {
196        self.add_contract_named(contract, None)
197    }
198}
199
200impl ContractProvider for DeployedContractsContainer {
201    fn contract_ref<T: OdraContract + 'static>(
202        &self,
203        env: &HostEnv
204    ) -> Result<T::HostRef, ContractError> {
205        self.contract_ref_named::<T>(env, None)
206    }
207
208    fn contract_ref_named<T: OdraContract + 'static>(
209        &self,
210        env: &HostEnv,
211        package_name: Option<String>
212    ) -> Result<T::HostRef, ContractError> {
213        let name = package_name.unwrap_or(T::HostRef::ident());
214        self.data
215            .borrow()
216            .contracts()
217            .iter()
218            .find(|c| c.key_name() == name)
219            .map(|c| Address::from_str(&c.package_hash).ok())
220            .and_then(|opt| opt.map(|addr| <T as HostRefLoader<T::HostRef>>::load(env, addr)))
221            .ok_or(ContractError::NotFound(T::HostRef::ident()))
222    }
223
224    fn all_contracts(&self) -> Vec<DeployedContract> {
225        self.data.borrow().contracts().clone()
226    }
227
228    fn address_by_name(&self, package_name: &str) -> Option<Address> {
229        self.data
230            .borrow()
231            .contracts()
232            .iter()
233            .find(|c| c.key_name() == package_name)
234            .and_then(|c| Address::from_str(&c.package_hash).ok())
235    }
236}
237
238/// This struct represents a contract in the `deployed_contracts.toml` file.
239#[derive(Deserialize, Serialize, Debug, Clone)]
240pub struct DeployedContract {
241    name: String,
242    #[serde(default)]
243    package_name: String,
244    package_hash: String
245}
246
247impl DeployedContract {
248    fn new<T: HasIdent>(address: Address, name: Option<String>) -> Self {
249        let contract_name = name.unwrap_or_else(|| T::ident());
250        Self {
251            name: T::ident(),
252            package_name: contract_name,
253            package_hash: address.to_string()
254        }
255    }
256
257    pub fn key_name(&self) -> String {
258        if self.package_name.is_empty() {
259            self.name.clone()
260        } else {
261            self.package_name.clone()
262        }
263    }
264
265    pub fn name(&self) -> String {
266        self.name.clone()
267    }
268
269    pub fn address(&self) -> Address {
270        Address::from_str(&self.package_hash).unwrap()
271    }
272}
273
274#[derive(Deserialize, Serialize, Debug, Clone)]
275pub(crate) struct ContractsData {
276    #[serde(alias = "time")]
277    last_updated: String,
278    contracts: Vec<DeployedContract>
279}
280
281impl Default for ContractsData {
282    fn default() -> Self {
283        Self {
284            last_updated: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
285            contracts: Vec::new()
286        }
287    }
288}
289
290impl ContractsData {
291    pub fn add_contract<T: HasIdent>(
292        &mut self,
293        address: Address,
294        package_name: Option<String>
295    ) -> Result<(), ContractError> {
296        let contract = DeployedContract::new::<T>(address, package_name);
297
298        // Check if a contract with this package_name already exists
299        if self
300            .contracts
301            .iter()
302            .any(|c| c.package_name == contract.package_name)
303        {
304            return Err(ContractError::ContractExists(contract.package_name));
305        }
306
307        self.contracts.push(contract);
308        self.last_updated = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
309        Ok(())
310    }
311
312    fn contracts(&self) -> &Vec<DeployedContract> {
313        &self.contracts
314    }
315}