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
35pub(crate) trait ContractStorage {
38 fn read(&self) -> Result<ContractsData, ContractError>;
40 fn write(&mut self, data: &ContractsData) -> Result<(), ContractError>;
42 fn backup(&self) -> Result<(), ContractError>;
44}
45
46pub(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
101pub trait ContractProvider {
103 fn contract_ref<T: OdraContract + 'static>(
107 &self,
108 env: &HostEnv
109 ) -> Result<T::HostRef, ContractError>;
110
111 fn contract_ref_named<T: OdraContract + 'static>(
115 &self,
116 env: &HostEnv,
117 name: Option<String>
118 ) -> Result<T::HostRef, ContractError>;
119
120 fn all_contracts(&self) -> Vec<DeployedContract>;
122
123 fn address_by_name(&self, name: &str) -> Option<Address>;
125}
126
127pub struct DeployedContractsContainer {
135 data: std::cell::RefCell<ContractsData>,
136 storage: std::cell::RefCell<Box<dyn ContractStorage>>
137}
138
139impl DeployedContractsContainer {
140 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 _ => {} }
174 Ok(())
175 }
176
177 pub fn add_contract_named<T: HostRef + HasIdent>(
179 &self,
180 contract: &T,
181 package_name: Option<String>
182 ) -> Result<(), ContractError> {
183 self.data
185 .borrow_mut()
186 .add_contract::<T>(contract.address(), package_name)?;
187
188 let data = self.data.borrow();
190 let mut storage = self.storage.borrow_mut();
191 storage.write(&data)
192 }
193
194 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#[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 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}