use std::{fs::File, io::Write, path::PathBuf, str::FromStr};
use chrono::{SecondsFormat, Utc};
use odra::{
contract_def::HasIdent,
host::{HostEnv, HostRef, HostRefLoader},
prelude::{Address, Addressable},
OdraContract
};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;
use crate::{
cmd::args::{DEPLOY_MODE_ARCHIVE, DEPLOY_MODE_OVERRIDE},
log,
utils::get_default_contracts_file
};
#[derive(Error, Debug)]
pub enum ContractError {
#[error("TOML serialization error")]
TomlSerialize(#[from] toml::ser::Error),
#[error("TOML deserialization error")]
TomlDeserialize(#[from] toml::de::Error),
#[error("Couldn't read file")]
Io(#[from] std::io::Error),
#[error("Couldn't find contract `{0}`")]
NotFound(String),
#[error("Couldn't find schema file for contract `{0}`")]
SchemaFileNotFound(String),
#[error("Contract `{0}` already exists")]
ContractExists(String)
}
pub(crate) trait ContractStorage {
fn read(&self) -> Result<ContractsData, ContractError>;
fn write(&mut self, data: &ContractsData) -> Result<(), ContractError>;
fn backup(&self) -> Result<(), ContractError>;
}
pub(crate) struct FileContractStorage {
file_path: PathBuf
}
impl FileContractStorage {
pub fn new(custom_path: Option<PathBuf>) -> Result<Self, ContractError> {
let mut path = project_root::get_project_root().map_err(ContractError::Io)?;
match &custom_path {
Some(path_str) if !path_str.to_str().unwrap_or_default().is_empty() => {
path.push(path_str);
}
_ => {
let default_file = get_default_contracts_file();
path.push(&default_file);
}
}
if !path.exists() {
let parent_path = path.parent().ok_or_else(|| {
ContractError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Parent directory not found"
))
})?;
std::fs::create_dir_all(parent_path).map_err(ContractError::Io)?;
}
Ok(Self { file_path: path })
}
}
impl ContractStorage for FileContractStorage {
fn read(&self) -> Result<ContractsData, ContractError> {
let file = std::fs::read_to_string(&self.file_path).map_err(ContractError::Io)?;
toml::from_str(&file).map_err(ContractError::TomlDeserialize)
}
fn write(&mut self, data: &ContractsData) -> Result<(), ContractError> {
let content = toml::to_string_pretty(&data).map_err(ContractError::TomlSerialize)?;
let mut file = File::create(&self.file_path).map_err(ContractError::Io)?;
file.write_all(content.as_bytes())
.map_err(ContractError::Io)?;
Ok(())
}
fn backup(&self) -> Result<(), ContractError> {
let mut new_path = self.file_path.with_extension("old");
while new_path.exists() {
new_path = new_path.with_added_extension("old");
}
std::fs::copy(&self.file_path, new_path).map_err(ContractError::Io)?;
Ok(())
}
}
pub trait ContractProvider {
fn contract_ref<T: OdraContract + 'static>(
&self,
env: &HostEnv
) -> Result<T::HostRef, ContractError>;
fn contract_ref_named<T: OdraContract + 'static>(
&self,
env: &HostEnv,
name: Option<String>
) -> Result<T::HostRef, ContractError>;
fn all_contracts(&self) -> Vec<DeployedContract>;
fn address_by_name(&self, name: &str) -> Option<Address>;
}
pub struct DeployedContractsContainer {
data: std::cell::RefCell<ContractsData>,
storage: std::cell::RefCell<Box<dyn ContractStorage>>
}
impl DeployedContractsContainer {
pub(crate) fn instance(storage: impl ContractStorage + 'static) -> Self {
match storage.read() {
Ok(data) => Self {
data: std::cell::RefCell::new(data),
storage: std::cell::RefCell::new(Box::new(storage))
},
Err(_) => Self {
data: std::cell::RefCell::new(Default::default()),
storage: std::cell::RefCell::new(Box::new(storage))
}
}
}
pub fn apply_deploy_mode(&self, mode: String) -> Result<(), ContractError> {
match mode.as_str() {
DEPLOY_MODE_OVERRIDE => {
self.data.borrow_mut().contracts.clear();
let data = self.data.borrow();
let mut storage = self.storage.borrow_mut();
storage.write(&data)?;
log("Contracts configuration has been overridden");
}
DEPLOY_MODE_ARCHIVE => {
let storage = self.storage.borrow_mut();
storage.backup()?;
self.data.borrow_mut().contracts.clear();
let data = self.data.borrow();
let mut storage = self.storage.borrow_mut();
storage.write(&data)?;
log("Starting fresh deployment. Previous contracts configuration has been backed up.");
}
_ => {} }
Ok(())
}
pub fn add_contract_named<T: HostRef + HasIdent>(
&self,
contract: &T,
package_name: Option<String>
) -> Result<(), ContractError> {
self.data
.borrow_mut()
.add_contract::<T>(contract.address(), package_name)?;
let data = self.data.borrow();
let mut storage = self.storage.borrow_mut();
storage.write(&data)
}
pub fn add_contract<T: HostRef + HasIdent>(&self, contract: &T) -> Result<(), ContractError> {
self.add_contract_named(contract, None)
}
}
impl ContractProvider for DeployedContractsContainer {
fn contract_ref<T: OdraContract + 'static>(
&self,
env: &HostEnv
) -> Result<T::HostRef, ContractError> {
self.contract_ref_named::<T>(env, None)
}
fn contract_ref_named<T: OdraContract + 'static>(
&self,
env: &HostEnv,
package_name: Option<String>
) -> Result<T::HostRef, ContractError> {
let name = package_name.unwrap_or(T::HostRef::ident());
self.data
.borrow()
.contracts()
.iter()
.find(|c| c.key_name() == name)
.map(|c| Address::from_str(&c.package_hash).ok())
.and_then(|opt| opt.map(|addr| <T as HostRefLoader<T::HostRef>>::load(env, addr)))
.ok_or(ContractError::NotFound(T::HostRef::ident()))
}
fn all_contracts(&self) -> Vec<DeployedContract> {
self.data.borrow().contracts().clone()
}
fn address_by_name(&self, package_name: &str) -> Option<Address> {
self.data
.borrow()
.contracts()
.iter()
.find(|c| c.key_name() == package_name)
.and_then(|c| Address::from_str(&c.package_hash).ok())
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DeployedContract {
name: String,
#[serde(default)]
package_name: String,
package_hash: String
}
impl DeployedContract {
fn new<T: HasIdent>(address: Address, name: Option<String>) -> Self {
let contract_name = name.unwrap_or_else(|| T::ident());
Self {
name: T::ident(),
package_name: contract_name,
package_hash: address.to_string()
}
}
pub fn key_name(&self) -> String {
if self.package_name.is_empty() {
self.name.clone()
} else {
self.package_name.clone()
}
}
pub fn name(&self) -> String {
self.name.clone()
}
pub fn address(&self) -> Address {
Address::from_str(&self.package_hash).unwrap()
}
}
#[derive(Deserialize, Serialize, Debug, Clone)]
pub(crate) struct ContractsData {
#[serde(alias = "time")]
last_updated: String,
contracts: Vec<DeployedContract>
}
impl Default for ContractsData {
fn default() -> Self {
Self {
last_updated: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
contracts: Vec::new()
}
}
}
impl ContractsData {
pub fn add_contract<T: HasIdent>(
&mut self,
address: Address,
package_name: Option<String>
) -> Result<(), ContractError> {
let contract = DeployedContract::new::<T>(address, package_name);
if self
.contracts
.iter()
.any(|c| c.package_name == contract.package_name)
{
return Err(ContractError::ContractExists(contract.package_name));
}
self.contracts.push(contract);
self.last_updated = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
Ok(())
}
fn contracts(&self) -> &Vec<DeployedContract> {
&self.contracts
}
}