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
53pub(crate) trait ContractStorage {
56 fn read(&self) -> Result<ContractsData, ContractError>;
58 fn write(&mut self, data: &ContractsData) -> Result<(), ContractError>;
60 fn backup(&self) -> Result<(), ContractError>;
62 fn source(&self) -> ContractStorageSource;
63}
64
65pub(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
126pub trait ContractProvider {
128 fn contract_ref<T: OdraContract + 'static>(
132 &self,
133 env: &HostEnv
134 ) -> Result<T::HostRef, ContractError>;
135
136 fn contract_ref_named<T: OdraContract + 'static>(
140 &self,
141 env: &HostEnv,
142 name: Option<String>
143 ) -> Result<T::HostRef, ContractError>;
144
145 fn all_contracts(&self) -> Vec<DeployedContract>;
147
148 fn address_by_name(&self, name: &str) -> Option<Address>;
150}
151
152pub struct DeployedContractsContainer {
160 data: std::cell::RefCell<ContractsData>,
161 storage: std::cell::RefCell<Box<dyn ContractStorage>>
162}
163
164impl DeployedContractsContainer {
165 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 _ => {} }
199 Ok(())
200 }
201
202 pub fn add_contract_named<T: HostRef + HasIdent>(
204 &self,
205 contract: &T,
206 package_name: Option<String>
207 ) -> Result<(), ContractError> {
208 self.data
210 .borrow_mut()
211 .add_contract::<T>(contract.address(), package_name)?;
212
213 let data = self.data.borrow();
215 let mut storage = self.storage.borrow_mut();
216 storage.write(&data)
217 }
218
219 pub fn add_contract<T: HostRef + HasIdent>(&self, contract: &T) -> Result<(), ContractError> {
221 self.add_contract_named(contract, None)
222 }
223
224 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#[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 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}