use odra::{
contract_def::HasIdent,
host::{Deployer, HostEnv, InstallConfig},
prelude::Addressable,
OdraContract
};
use crate::{
container::{ContractError, ContractStorage, FileContractStorage},
ContractProvider, DeployedContractsContainer
};
use std::path::PathBuf;
const DEFAULT_CONTRACTS_FILE: &str = "resources/contracts.toml";
pub fn log<T: ToString>(msg: T) {
prettycli::info(&msg.to_string());
}
pub(crate) fn get_default_contracts_file() -> String {
if let Ok(network_name) = std::env::var("ODRA_CASPER_LIVENET_CHAIN_NAME") {
format!("resources/{}-contracts.toml", network_name)
} else {
DEFAULT_CONTRACTS_FILE.to_string()
}
}
pub trait DeployerExt: Sized {
type Contract: OdraContract + 'static + Deployer<Self::Contract>;
fn load_or_deploy(
env: &HostEnv,
args: <<Self as DeployerExt>::Contract as OdraContract>::InitArgs,
container: &mut DeployedContractsContainer,
gas: u64
) -> Result<<<Self as DeployerExt>::Contract as OdraContract>::HostRef, crate::deploy::Error>
{
if let Ok(contract) = container.contract_ref::<Self::Contract>(env) {
prettycli::info(&format!(
"Using existing contract {} at address {}",
<Self::Contract as OdraContract>::HostRef::ident(),
contract.address().to_string()
));
Ok(contract)
} else {
env.set_gas(gas);
let contract = Self::Contract::try_deploy(env, args)?;
env.set_gas(0);
container.add_contract(&contract)?;
Ok(contract)
}
}
fn load_or_deploy_with_cfg(
env: &HostEnv,
package_name: Option<String>,
args: <<Self as DeployerExt>::Contract as OdraContract>::InitArgs,
cfg: InstallConfig,
container: &mut DeployedContractsContainer,
gas: u64
) -> Result<<<Self as DeployerExt>::Contract as OdraContract>::HostRef, crate::deploy::Error>
{
if let Ok(contract) =
container.contract_ref_named::<Self::Contract>(env, package_name.clone())
{
prettycli::info(&format!(
"Using existing contract {} at address {}",
<Self::Contract as OdraContract>::HostRef::ident(),
contract.address().to_string()
));
Ok(contract)
} else {
env.set_gas(gas);
let contract = Self::Contract::try_deploy_with_cfg(env, args, cfg)?;
env.set_gas(0);
container.add_contract_named(&contract, package_name)?;
Ok(contract)
}
}
}
impl<T: OdraContract + Deployer<T> + 'static> DeployerExt for T {
type Contract = T;
}
pub trait ContractLoaderExt: Sized {
type Contract: OdraContract + 'static;
fn load_from_file(
env: &HostEnv,
path: impl Into<PathBuf>
) -> Result<<Self::Contract as OdraContract>::HostRef, crate::deploy::Error> {
Self::load_from_file_named(env, path, None)
}
fn load_from_file_named(
env: &HostEnv,
path: impl Into<PathBuf>,
package_name: Option<String>
) -> Result<<Self::Contract as OdraContract>::HostRef, crate::deploy::Error> {
let container = container_from_file(path.into())?;
Ok(container.contract_ref_named::<Self::Contract>(env, package_name)?)
}
fn load_from_default_file(
env: &HostEnv
) -> Result<<Self::Contract as OdraContract>::HostRef, crate::deploy::Error> {
Self::load_from_file(env, get_default_contracts_file())
}
}
impl<T: OdraContract + 'static> ContractLoaderExt for T {
type Contract = T;
}
pub(crate) fn container_from_file(
path: PathBuf
) -> Result<DeployedContractsContainer, ContractError> {
let storage = FileContractStorage::new(Some(path))?;
let data = storage.read()?;
Ok(DeployedContractsContainer::with_data(data, storage))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::container::ContractProvider;
fn temp_file(name: &str, content: &str) -> PathBuf {
let path = std::env::temp_dir().join(format!("odra-cli-{}-{name}", std::process::id()));
std::fs::write(&path, content).unwrap();
path
}
#[test]
fn missing_file_is_an_error() {
let path = std::env::temp_dir().join("odra-cli-does-not-exist.toml");
let err = container_from_file(path).err().unwrap();
assert!(matches!(err, ContractError::Io(_)), "{err:?}");
}
#[test]
fn malformed_file_is_an_error() {
let path = temp_file("bad.toml", "this is not = [toml");
let err = container_from_file(path).err().unwrap();
assert!(matches!(err, ContractError::TomlDeserialize(_)), "{err:?}");
}
#[test]
fn contracts_are_read_from_the_file() {
let path = temp_file(
"ok.toml",
r#"
time = "2026-09-17"
[[contracts]]
name = "Token"
package_name = "Token"
package_hash = "hash-0101010101010101010101010101010101010101010101010101010101010101"
"#
);
let container = container_from_file(path).unwrap();
assert!(container.address_by_name("Token").is_some());
assert!(container.address_by_name("Other").is_none());
}
}