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
13pub const DEPLOYED_CONTRACTS_FILE: &str = "resources/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    #[error("Couldn't find schema file for contract `{0}`")]
26    SchemaFileNotFound(String)
27}
28
29/// Represents storage for deployed contracts.
30/// This trait defines the methods for reading and writing contract data.
31pub(crate) trait ContractStorage {
32    /// Reads the contract data from the storage.
33    fn read(&self) -> Result<ContractsData, ContractError>;
34    /// Writes the contract data to the storage.
35    fn write(&mut self, data: &ContractsData) -> Result<(), ContractError>;
36}
37
38/// Represents the data structure for storing deployed contracts in a TOML file.
39pub(crate) struct FileContractStorage {
40    file_path: PathBuf
41}
42
43impl FileContractStorage {
44    pub fn new(custom_path: Option<PathBuf>) -> Result<Self, ContractError> {
45        let mut path = project_root::get_project_root().map_err(ContractError::Io)?;
46        match &custom_path {
47            Some(path_str) if !path_str.to_str().unwrap_or_default().is_empty() => {
48                path.push(path_str);
49            }
50            _ => path.push(DEPLOYED_CONTRACTS_FILE)
51        }
52        if !path.exists() {
53            let parent_path = path.parent().ok_or_else(|| {
54                ContractError::Io(std::io::Error::new(
55                    std::io::ErrorKind::NotFound,
56                    "Parent directory not found"
57                ))
58            })?;
59            std::fs::create_dir_all(parent_path).map_err(ContractError::Io)?;
60        }
61
62        Ok(Self { file_path: path })
63    }
64}
65
66impl ContractStorage for FileContractStorage {
67    fn read(&self) -> Result<ContractsData, ContractError> {
68        let file = std::fs::read_to_string(&self.file_path).map_err(ContractError::Io)?;
69        toml::from_str(&file).map_err(ContractError::TomlDeserialize)
70    }
71
72    fn write(&mut self, data: &ContractsData) -> Result<(), ContractError> {
73        let content = toml::to_string_pretty(&data).map_err(ContractError::TomlSerialize)?;
74        let mut file = File::create(&self.file_path).map_err(ContractError::Io)?;
75        file.write_all(content.as_bytes())
76            .map_err(ContractError::Io)?;
77        Ok(())
78    }
79}
80
81/// This trait defines the methods for providing access to deployed contracts.
82pub trait ContractProvider {
83    /// Gets a reference to the contract.
84    ///
85    /// Returns a reference to the contract if it is found, otherwise returns an error.
86    fn contract_ref<T: OdraContract + 'static>(
87        &self,
88        env: &HostEnv
89    ) -> Result<T::HostRef, ContractError>;
90
91    /// Returns a list of all deployed contracts with their names and addresses.
92    fn all_contracts(&self) -> Vec<(String, Address)>;
93
94    /// Returns the contract address.
95    fn address_by_name(&self, name: &str) -> Option<Address>;
96}
97
98/// Struct representing the deployed contracts.
99///
100/// This struct is used to store the contracts name and address at the deploy
101/// time and to retrieve a reference to the contract at runtime.
102///
103/// The data is stored in a TOML file `deployed_contracts.toml` in the
104/// `{project_root}/resources` directory.
105pub struct DeployedContractsContainer {
106    data: ContractsData,
107    storage: Box<dyn ContractStorage>
108}
109
110impl DeployedContractsContainer {
111    /// Creates a new instance.
112    pub(crate) fn instance(storage: impl ContractStorage + 'static) -> Self {
113        match storage.read() {
114            Ok(data) => Self {
115                data,
116                storage: Box::new(storage)
117            },
118            Err(_) => Self {
119                data: Default::default(),
120                storage: Box::new(storage)
121            }
122        }
123    }
124
125    /// Adds a contract to the container.
126    pub fn add_contract<T: HostRef + HasIdent>(
127        &mut self,
128        contract: &T
129    ) -> Result<(), ContractError> {
130        self.data.add_contract::<T>(contract.address());
131        self.storage.write(&self.data)
132    }
133}
134
135impl ContractProvider for DeployedContractsContainer {
136    fn contract_ref<T: OdraContract + 'static>(
137        &self,
138        env: &HostEnv
139    ) -> Result<T::HostRef, ContractError> {
140        self.data
141            .contracts()
142            .iter()
143            .find(|c| c.name == T::HostRef::ident())
144            .map(|c| Address::from_str(&c.package_hash).ok())
145            .and_then(|opt| opt.map(|addr| <T as HostRefLoader<T::HostRef>>::load(env, addr)))
146            .ok_or(ContractError::NotFound(T::HostRef::ident()))
147    }
148
149    fn all_contracts(&self) -> Vec<(String, Address)> {
150        self.data
151            .contracts()
152            .iter()
153            .filter_map(|c| {
154                Address::from_str(&c.package_hash)
155                    .ok()
156                    .map(|addr| (c.name.clone(), addr))
157            })
158            .collect()
159    }
160
161    fn address_by_name(&self, name: &str) -> Option<Address> {
162        self.data
163            .contracts()
164            .iter()
165            .find(|c| c.name == name)
166            .and_then(|c| Address::from_str(&c.package_hash).ok())
167    }
168}
169
170/// This struct represents a contract in the `deployed_contracts.toml` file.
171#[derive(Deserialize, Serialize, Debug, Clone)]
172struct DeployedContract {
173    name: String,
174    package_hash: String
175}
176
177impl DeployedContract {
178    fn new<T: HasIdent>(address: Address) -> Self {
179        Self {
180            name: T::ident(),
181            package_hash: address.to_string()
182        }
183    }
184}
185
186#[derive(Deserialize, Serialize, Debug, Clone)]
187pub(crate) struct ContractsData {
188    #[serde(alias = "time")]
189    last_updated: String,
190    contracts: Vec<DeployedContract>
191}
192
193impl Default for ContractsData {
194    fn default() -> Self {
195        Self {
196            last_updated: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
197            contracts: Vec::new()
198        }
199    }
200}
201
202impl ContractsData {
203    pub fn add_contract<T: HasIdent>(&mut self, address: Address) {
204        let contract = DeployedContract::new::<T>(address);
205        self.contracts.retain(|c| c.name != contract.name);
206        self.contracts.push(contract);
207        self.last_updated = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
208    }
209
210    fn contracts(&self) -> &Vec<DeployedContract> {
211        &self.contracts
212    }
213}