naumachia 0.2.0

Cardano Smart-Contract Framework
Documentation
use crate::{
    error::*,
    ledger_client::{LedgerClient, LedgerClientResult},
    output::Output,
    transaction::TxId,
    trireme_ledger_client::blockfrost_ledger::BlockfrostApiKey,
    trireme_ledger_client::raw_secret_phrase::RawSecretPhraseKeys,
    Address, UnbuiltTransaction,
};

use async_trait::async_trait;
use blockfrost_http_client::{MAINNET_URL, TEST_URL};
use cml_client::{
    blockfrost_ledger::BlockFrostLedger, plutus_data_interop::PlutusDataInterop, CMLLedgerCLient,
};
use dirs::home_dir;
use serde::{de::DeserializeOwned, ser, Deserialize, Serialize};
use std::fmt::Debug;
use std::{marker::PhantomData, path::PathBuf};
use thiserror::Error;
use tokio::{fs, io::AsyncWriteExt};

pub mod blockfrost_ledger;
pub mod cml_client;
pub mod raw_secret_phrase;

pub const TRIREME_CONFIG_FOLDER: &str = ".trireme";
pub const TRIREME_CONFIG_FILE: &str = "config.toml";

pub fn path_to_trireme_config_dir() -> Result<PathBuf> {
    let mut dir =
        home_dir().ok_or_else(|| Error::Trireme("Could not find home directory :(".to_string()))?;
    dir.push(TRIREME_CONFIG_FOLDER);
    Ok(dir)
}

pub fn path_to_trireme_config_file() -> Result<PathBuf> {
    let mut dir = path_to_trireme_config_dir()?;
    dir.push(TRIREME_CONFIG_FILE);
    Ok(dir)
}

// TODO: PlutusDataInterop is prolly overconstraining for the Redeemer
pub async fn get_trireme_ledger_client_from_file<
    Datum: PlutusDataInterop,
    Redeemer: PlutusDataInterop,
>() -> Result<TriremeLedgerClient<Datum, Redeemer>> {
    let file_path = path_to_trireme_config_file()?;
    if let Some(config) = read_toml_struct_from_file::<TriremeConfig>(&file_path).await? {
        let ledger_client = config.to_client().await?;
        Ok(ledger_client)
    } else {
        Err(Error::Trireme(
            "Trireme not initialized (config not found)".to_string(),
        ))
    }
}

#[derive(Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum LedgerSource {
    BlockFrost { api_key_file: PathBuf },
}

#[derive(Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum KeySource {
    RawSecretPhrase { phrase_file: PathBuf },
}

#[derive(Deserialize, Serialize)]
#[serde(tag = "type")]
pub enum Network {
    Testnet,
    Mainnet,
}

impl From<Network> for u8 {
    fn from(network: Network) -> Self {
        match network {
            Network::Testnet => 0,
            Network::Mainnet => 1,
        }
    }
}

#[derive(Deserialize, Serialize)]
pub struct TriremeConfig {
    ledger_source: LedgerSource,
    key_source: KeySource,
    network: Network,
}

impl TriremeConfig {
    pub fn new(ledger_source: LedgerSource, key_source: KeySource, network: Network) -> Self {
        TriremeConfig {
            ledger_source,
            key_source,
            network,
        }
    }

    pub async fn to_client<Datum: PlutusDataInterop, Redeemer: PlutusDataInterop>(
        self,
    ) -> Result<TriremeLedgerClient<Datum, Redeemer>> {
        let network = self.network;
        let ledger = match self.ledger_source {
            LedgerSource::BlockFrost { api_key_file } => {
                let blockfrost_key = read_toml_struct_from_file::<BlockfrostApiKey>(&api_key_file)
                    .await?
                    .ok_or_else(|| {
                        Error::Trireme(
                            "Couldn't find blockfrost config, please try reinitialize Trireme"
                                .to_string(),
                        )
                    })?;
                let key: String = blockfrost_key.into();
                let url = match network {
                    Network::Testnet => TEST_URL,
                    Network::Mainnet => MAINNET_URL,
                };
                BlockFrostLedger::new(url, &key)
            }
        };
        let network_index = network.into();
        let keys = match self.key_source {
            KeySource::RawSecretPhrase { phrase_file } => {
                RawSecretPhraseKeys::new(phrase_file, network_index)
            }
        };

        let inner_client = InnerClient::Cml(CMLLedgerCLient::new(ledger, keys, network_index));
        let trireme_client = TriremeLedgerClient {
            _datum: Default::default(),
            _redeemer: Default::default(),
            inner_client,
        };
        Ok(trireme_client)
    }
}

enum InnerClient<Datum: PlutusDataInterop, Redeemer: PlutusDataInterop> {
    Cml(CMLLedgerCLient<BlockFrostLedger, RawSecretPhraseKeys, Datum, Redeemer>),
}

pub struct TriremeLedgerClient<Datum: PlutusDataInterop, Redeemer: PlutusDataInterop> {
    _datum: PhantomData<Datum>,
    _redeemer: PhantomData<Redeemer>,
    inner_client: InnerClient<Datum, Redeemer>,
}

#[async_trait]
impl<Datum: PlutusDataInterop + Send + Sync + Debug, Redeemer: PlutusDataInterop + Send + Sync>
    LedgerClient<Datum, Redeemer> for TriremeLedgerClient<Datum, Redeemer>
{
    async fn signer(&self) -> LedgerClientResult<Address> {
        match &self.inner_client {
            InnerClient::Cml(cml_client) => cml_client.signer(),
        }
        .await
    }

    async fn outputs_at_address(
        &self,
        address: &Address,
        count: usize,
    ) -> LedgerClientResult<Vec<Output<Datum>>> {
        match &self.inner_client {
            InnerClient::Cml(cml_client) => cml_client.outputs_at_address(address, count),
        }
        .await
    }

    async fn all_outputs_at_address(
        &self,
        address: &Address,
    ) -> LedgerClientResult<Vec<Output<Datum>>> {
        match &self.inner_client {
            InnerClient::Cml(cml_client) => cml_client.all_outputs_at_address(address),
        }
        .await
    }

    async fn issue(&self, tx: UnbuiltTransaction<Datum, Redeemer>) -> LedgerClientResult<TxId> {
        match &self.inner_client {
            InnerClient::Cml(cml_client) => cml_client.issue(tx),
        }
        .await
    }
}

#[derive(Debug, Error)]
pub enum TomlError {
    #[error("No config directory for raw phrase file: {0:?}")]
    NoParentDir(String),
}

pub async fn write_toml_struct_to_file<Toml: ser::Serialize>(
    file_path: &PathBuf,
    toml_struct: &Toml,
) -> Result<()> {
    let serialized = toml::to_string(&toml_struct).map_err(|e| Error::TOML(Box::new(e)))?;
    let parent_dir = file_path
        .parent()
        .ok_or_else(|| TomlError::NoParentDir(format!("{:?}", file_path)))
        .map_err(|e| Error::TOML(Box::new(e)))?;
    fs::create_dir_all(&parent_dir)
        .await
        .map_err(|e| Error::TOML(Box::new(e)))?;
    let mut file = fs::OpenOptions::new()
        .write(true)
        .create(true)
        .open(&file_path)
        .await
        .map_err(|e| Error::TOML(Box::new(e)))?;
    file.write_all(&serialized.into_bytes())
        .await
        .map_err(|e| Error::TOML(Box::new(e)))?;
    Ok(())
}

pub async fn read_toml_struct_from_file<Toml: DeserializeOwned>(
    file_path: &PathBuf,
) -> Result<Option<Toml>> {
    if file_path.exists() {
        let contents = fs::read_to_string(file_path)
            .await
            .map_err(|e| Error::TOML(Box::new(e)))?;
        let toml_struct = toml::from_str(&contents).map_err(|e| Error::TOML(Box::new(e)))?;
        Ok(Some(toml_struct))
    } else {
        Ok(None)
    }
}