Skip to main content

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