piecework_cli 0.2.0

Client to interact with a piecework application running on holochain
Documentation
use crate::CommonOpts;
use crate::{display::format_table, ham::Ham};
use anyhow::Result;
use colored::Colorize;
use dialoguer::{Confirm, Input, Select};
use holochain_client::Timestamp;
use holochain_types::dna::ActionHash;
use rave_engine::types::entries::{
    AddressBook, PieceDefinition, PieceDefinitionExt, PoolDefinition, PoolDefinitionExt,
    SystemRaveAgreements, TransactionFeeCompute,
};
use rave_engine::types::{ActionHashB64, AgentPubKeyB64};
use serde_json::Value;
use std::str::FromStr;
use std::time::Duration;
use zfuel::fuel::ZFuel;

pub async fn initialize_pool_definition(
    value: CommonOpts,
    effective_start_date: Option<u64>,
    expiration_duration_days: Option<u64>,
    pay_transaction_fee_agreement: Option<ActionHashB64>,
    compute_credit_limit_agreement: Option<ActionHashB64>,
    executor: Option<Option<AddressBook>>,
    additional_rave_agreements: Option<Vec<ActionHashB64>>,
) -> Result<()> {
    let agent = Ham::connect(value.clone()).await?;
    println!("{}", "\nPool Definition Setup".bold().underline());

    // 1. Get effective start date
    let effective_start_date = if effective_start_date.is_none() {
        let use_current = Select::new()
            .with_prompt("When should this pool definition become effective?")
            .items(&["Use current time", "Specify custom date"])
            .default(0)
            .interact()?;

        if use_current == 0 {
            None
        } else {
            let days: u64 = Input::new()
                .with_prompt("Enter number of days from now")
                .default(0)
                .interact()?;
            Some(Timestamp::now().as_micros() as u64 + (days * 24 * 60 * 60 * 1_000_000))
        }
    } else {
        effective_start_date
    };

    // 2. Get expiration duration
    let expiration_duration_days = if expiration_duration_days.is_none() {
        let days: u64 = Input::new()
            .with_prompt("Enter pool definition duration in days (default 30)")
            .default(30)
            .interact()?;
        Some(days)
    } else {
        expiration_duration_days
    };

    // 3. Get agreements if not provided
    let pay_transaction_fee_agreement = if pay_transaction_fee_agreement.is_none() {
        let input: String = Input::new()
            .with_prompt("Enter transaction fee agreement hash")
            .interact()?;
        Some(ActionHashB64::from_str(&input)?)
    } else {
        pay_transaction_fee_agreement
    };

    let compute_credit_limit_agreement = if compute_credit_limit_agreement.is_none() {
        let input: String = Input::new()
            .with_prompt("Enter credit limit agreement hash")
            .interact()?;
        Some(ActionHashB64::from_str(&input)?)
    } else {
        compute_credit_limit_agreement
    };

    // 4. Get special agents
    let executor = if executor.is_none() {
        let mut agents: Option<AddressBook> = None;

        println!("\n{}", "Special Agents Setup".bold());
        println!("These agents will be authorized to collect transaction fees");

        loop {
            let action = Select::new()
                .with_prompt("Executor Menu")
                .items(&["Add agent", "Done"])
                .default(0)
                .interact()?;

            match action {
                0 => {
                    // Add agent
                    let agent: String = Input::new()
                        .with_prompt("Enter agent public key")
                        .interact()?;

                    match AgentPubKeyB64::from_str(&agent) {
                        Ok(agent_key) => {
                            let address_book = AddressBook {
                                pub_key: agent_key,
                                address_book_data: Value::Null,
                            };
                            agents = Some(address_book);
                            println!("✓ Agent added");
                        }
                        Err(_) => println!("✗ Invalid agent public key, try again"),
                    };
                }
                _ => break,
            }
        }

        if agents.is_none() {
            println!("\n{}", "Warning: No executor added. This means no one will be able to collect transaction fees.".yellow());
            if !Confirm::new()
                .with_prompt("Continue without executor?")
                .default(false)
                .interact()?
            {
                return Err(anyhow::anyhow!("Cancelled - no executor added"));
            }
        }

        agents
    } else {
        executor.unwrap()
    };

    // 5. Get additional rave agreements
    let additional_rave_agreements = if additional_rave_agreements.is_none() {
        let mut agreements = Vec::new();

        println!("\n{}", "Additional RAVE Agreements Setup".bold());
        println!("These are optional agreements that will be added to the pool");

        loop {
            let action = Select::new()
                .with_prompt("Additional Agreements Menu")
                .items(&[
                    "Add agreement",
                    "List agreements",
                    "Remove agreement",
                    "Done",
                ])
                .default(0)
                .interact()?;

            match action {
                0 => {
                    // Add agreement
                    let agreement_hash: String = Input::new()
                        .with_prompt("Enter agreement hash")
                        .interact()?;

                    match ActionHashB64::from_str(&agreement_hash) {
                        Ok(agreement_id) => {
                            // Get special agents for this agreement
                            let mut agreement_agents = Vec::new();

                            loop {
                                let agent_action = Select::new()
                                    .with_prompt("Special Agents for this Agreement")
                                    .items(&["Add agent", "List agents", "Remove agent", "Done"])
                                    .default(0)
                                    .interact()?;

                                match agent_action {
                                    0 => {
                                        let agent: String = Input::new()
                                            .with_prompt("Enter agent public key")
                                            .interact()?;

                                        match AgentPubKeyB64::from_str(&agent) {
                                            Ok(agent_key) => {
                                                agreement_agents.push(agent_key);
                                                println!("✓ Agent added");
                                            }
                                            Err(_) => println!("✗ Invalid agent public key"),
                                        }
                                    }
                                    1 => {
                                        if agreement_agents.is_empty() {
                                            println!("No agents added yet");
                                        } else {
                                            println!("\nCurrent agents for this agreement:");
                                            for (i, agent) in agreement_agents.iter().enumerate() {
                                                println!("{}. {}", i + 1, agent);
                                            }
                                        }
                                    }
                                    2 => {
                                        if agreement_agents.is_empty() {
                                            println!("No agents to remove");
                                            continue;
                                        }

                                        let choices: Vec<String> = agreement_agents
                                            .iter()
                                            .map(|a| a.to_string())
                                            .collect();

                                        let selection = Select::new()
                                            .with_prompt("Select agent to remove")
                                            .items(&choices)
                                            .interact()?;

                                        agreement_agents.remove(selection);
                                        println!("✓ Agent removed");
                                    }
                                    _ => break,
                                }
                            }

                            agreements.push(agreement_id.into());
                            println!("✓ Agreement added");
                        }
                        Err(_) => println!("✗ Invalid agreement hash"),
                    }
                }
                1 => {
                    // List agreements
                    if agreements.is_empty() {
                        println!("No agreements added yet");
                    } else {
                        println!("\nCurrent agreements:");
                        for (i, agreement) in agreements.iter().enumerate() {
                            println!("{}. Agreement ID: {}", i + 1, agreement);
                        }
                    }
                }
                2 => {
                    // Remove agreement
                    if agreements.is_empty() {
                        println!("No agreements to remove");
                        continue;
                    }

                    let choices: Vec<String> =
                        agreements.iter().map(|a| format!("ID: {} ", a)).collect();

                    let selection = Select::new()
                        .with_prompt("Select agreement to remove")
                        .items(&choices)
                        .interact()?;

                    agreements.remove(selection);
                    println!("✓ Agreement removed");
                }
                _ => break,
            }
        }

        agreements
    } else {
        additional_rave_agreements.unwrap()
    };

    // Create and submit pool definition
    let effective_start_date = if let Some(start) = effective_start_date {
        Timestamp::from_micros(start as i64)
    } else {
        Timestamp::now()
    };

    let days = expiration_duration_days.unwrap_or(30);
    let duration = Duration::from_secs(days * 24 * 60 * 60);
    let expiration_date = (effective_start_date + duration)?;

    let additional_special_agents = if let Some(executor) = executor.clone() {
        vec![executor]
    } else {
        vec![]
    };
    let pool_definition = PoolDefinition {
        effective_start_date,
        expiration_date,
        system_rave_agreements: SystemRaveAgreements {
            compute_credit_limit: compute_credit_limit_agreement.unwrap().into(),
            compute_transaction_fee: TransactionFeeCompute {
                agreement: pay_transaction_fee_agreement.unwrap().into(),
                fee_trigger: ZFuel::from_str("10").unwrap(),
                fee_percentage: 1,
            },
        },
        additional_special_agents,
        additional_rave_agreements: additional_rave_agreements.clone(),
    };

    // Show summary before submitting
    println!("\n{}", "Review Pool Definition".bold());
    println!("Effective Start: {}", effective_start_date);
    println!("Expiration Date: {}", expiration_date);
    println!("Executor: {:?}", executor);
    println!(
        "Additional Agreements: {}",
        additional_rave_agreements.len()
    );

    let confirm = Select::new()
        .with_prompt("Submit pool definition?")
        .items(&["Yes", "No"])
        .default(0)
        .interact()?;

    if confirm == 1 {
        println!("Cancelled");
        return Ok(());
    }

    let result: ActionHash = agent
        .zome_call(
            "alliance",
            "transactor",
            "initialize_pool_definition",
            pool_definition,
        )
        .await?;

    println!("\n{}", "Pool Definition Initialized:".green());
    println!("Action Hash: {}", result);

    Ok(())
}

pub async fn get_current_pool_definition(value: CommonOpts) -> Result<()> {
    let agent = Ham::connect(value).await?;

    let result: PoolDefinitionExt = agent
        .zome_call("alliance", "transactor", "get_current_pool_definition", ())
        .await?;

    println!("{}", "Pool Definitions:".bold());
    println!("{:?}", result);

    Ok(())
}

pub async fn add_pool_pieces(value: CommonOpts, piece_definition: PieceDefinition) -> Result<()> {
    let agent = Ham::connect(value).await?;

    let result: ActionHash = agent
        .zome_call(
            "alliance",
            "transactor",
            "add_pool_pieces",
            piece_definition,
        )
        .await?;

    println!("Pool Pieces Added:");
    println!("Action Hash: {:?}", result);

    Ok(())
}

pub async fn update_pool_pieces(
    value: CommonOpts,
    piece_definition: PieceDefinitionExt,
) -> Result<()> {
    let agent = Ham::connect(value).await?;

    let result: ActionHash = agent
        .zome_call(
            "alliance",
            "transactor",
            "update_pool_pieces",
            piece_definition,
        )
        .await?;

    println!("Pool Pieces Updated:");
    println!("Action Hash: {:?}", result);

    Ok(())
}

pub async fn get_pool_pieces_details(value: CommonOpts) -> Result<()> {
    let agent = Ham::connect(value).await?;

    let result: Vec<PieceDefinitionExt> = agent
        .zome_call("alliance", "transactor", "get_pool_pieces_details", ())
        .await?;

    println!("Pool Pieces Details:");
    if !result.is_empty() {
        let table = format_table(&result, "Pool Pieces");
        table.printstd();
    } else {
        println!("{}", "No pool pieces".dimmed());
    }

    Ok(())
}