odra_cli/
container.rs

1use std::{fs::File, io::Write, path::PathBuf, str::FromStr};
2
3use chrono::{DateTime, SecondsFormat, Utc};
4use odra::{
5    contract_def::HasIdent,
6    host::{HostEnv, HostRef, HostRefLoader},
7    prelude::Address,
8    OdraContract
9};
10use serde_derive::{Deserialize, Serialize};
11use thiserror::Error;
12
13const DEPLOYED_CONTRACTS_FILE: &str = "resources/deployed_contracts.toml";
14
15#[derive(Error, Debug)]
16pub enum ContractError {
17    #[error("TOML serialization error")]
18    TomlSerialize(#[from] toml::ser::Error),
19    #[error("TOML deserialization error")]
20    TomlDeserialize(#[from] toml::de::Error),
21    #[error("Couldn't read file")]
22    Io(#[from] std::io::Error),
23    #[error("Couldn't find contract `{0}`")]
24    NotFound(String)
25}
26
27/// Struct representing the deployed contracts.
28///
29/// This struct is used to store the contracts name and address at the deploy
30/// time and to retrieve a reference to the contract at runtime.
31///
32/// The data is stored in a TOML file `deployed_contracts.toml` in the
33/// `{project_root}/resources` directory.
34#[derive(Deserialize, Serialize, Debug, Clone)]
35pub struct DeployedContractsContainer {
36    time: String,
37    pub contracts: Vec<DeployedContract>
38}
39
40impl DeployedContractsContainer {
41    /// Creates a new instance.
42    pub(crate) fn new() -> Result<Self, ContractError> {
43        Self::handle_previous_version()?;
44        let now: DateTime<Utc> = Utc::now();
45        Ok(Self {
46            time: now.to_rfc3339_opts(SecondsFormat::Secs, true),
47            contracts: Vec::new()
48        })
49    }
50
51    /// Adds a contract to the container.
52    pub fn add_contract<T: HostRef + HasIdent>(
53        &mut self,
54        contract: &T
55    ) -> Result<(), ContractError> {
56        self.contracts
57            .push(DeployedContract::new::<T>(contract.address()));
58        self.update()
59    }
60
61    /// Gets reference to the contract.
62    ///
63    /// Returns a reference to the contract if it is found in the list, otherwise returns an error.
64    pub fn get_ref<T: OdraContract + 'static>(
65        &self,
66        env: &HostEnv
67    ) -> Result<T::HostRef, ContractError> {
68        self.contracts
69            .iter()
70            .find(|c| c.name == T::HostRef::ident())
71            .map(|c| Address::from_str(&c.package_hash).ok())
72            .and_then(|opt| opt.map(|addr| <T as HostRefLoader<T::HostRef>>::load(env, addr)))
73            .ok_or(ContractError::NotFound(T::HostRef::ident()))
74    }
75
76    /// Returns the contract address.
77    pub fn address(&self, name: &str) -> Option<Address> {
78        self.contracts
79            .iter()
80            .find(|c| c.name == name)
81            .and_then(|c| Address::from_str(&c.package_hash).ok())
82    }
83
84    /// Load from the file.
85    pub fn load() -> Result<Self, ContractError> {
86        let path = Self::file_path()?;
87        let file = std::fs::read_to_string(path).map_err(ContractError::Io)?;
88
89        let result = toml::from_str(&file).map_err(ContractError::TomlDeserialize)?;
90        Ok(result)
91    }
92
93    /// Backup previous version of the file.
94    pub(crate) fn handle_previous_version() -> Result<(), ContractError> {
95        if let Ok(deployed_contracts) = Self::load() {
96            // Build new file name.
97            let date = deployed_contracts.time();
98            let mut path = project_root::get_project_root().map_err(ContractError::Io)?;
99            path.push(format!("{}.{}", DEPLOYED_CONTRACTS_FILE, date));
100
101            // Store previous version under new file name.
102            deployed_contracts.save_at(&path)?;
103
104            // Remove old file.
105            std::fs::remove_file(path).map_err(ContractError::Io)?;
106        }
107        Ok(())
108    }
109
110    /// Save the file at the given path.
111    fn save_at(&self, file_path: &PathBuf) -> Result<(), ContractError> {
112        let content = toml::to_string_pretty(&self).map_err(ContractError::TomlSerialize)?;
113        let mut file = File::create(file_path).map_err(ContractError::Io)?;
114
115        file.write_all(content.as_bytes())
116            .map_err(ContractError::Io)?;
117        Ok(())
118    }
119
120    /// Return creation time.
121    fn time(&self) -> &str {
122        &self.time
123    }
124
125    /// Update the file.
126    fn update(&self) -> Result<(), ContractError> {
127        let path = Self::file_path()?;
128        self.save_at(&path)
129    }
130
131    fn file_path() -> Result<PathBuf, ContractError> {
132        let mut path = project_root::get_project_root().map_err(ContractError::Io)?;
133        path.push(DEPLOYED_CONTRACTS_FILE);
134
135        Ok(path)
136    }
137}
138
139/// This struct represents a contract in the `deployed_contracts.toml` file.
140#[derive(Deserialize, Serialize, Debug, Clone)]
141pub struct DeployedContract {
142    pub name: String,
143    pub package_hash: String
144}
145
146impl DeployedContract {
147    fn new<T: HasIdent>(address: &Address) -> Self {
148        Self {
149            name: T::ident(),
150            package_hash: address.to_string()
151        }
152    }
153}