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
29pub(crate) trait ContractStorage {
32 fn read(&self) -> Result<ContractsData, ContractError>;
34 fn write(&mut self, data: &ContractsData) -> Result<(), ContractError>;
36}
37
38pub(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
81pub trait ContractProvider {
83 fn contract_ref<T: OdraContract + 'static>(
87 &self,
88 env: &HostEnv
89 ) -> Result<T::HostRef, ContractError>;
90
91 fn all_contracts(&self) -> Vec<(String, Address)>;
93
94 fn address_by_name(&self, name: &str) -> Option<Address>;
96}
97
98pub struct DeployedContractsContainer {
106 data: ContractsData,
107 storage: Box<dyn ContractStorage>
108}
109
110impl DeployedContractsContainer {
111 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 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#[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}