urma-cli 0.1.1

Command-line tools for URMA identities, wallets, Wire, archives and Git
use crate::common_cli::publish_plan;
use crate::{key_cli::VaultAccess, node_cli::NodeArgs};
use bitcoin::{Block, BlockHash, Transaction, Txid, XOnlyPublicKey};
use clap::{Args, Subcommand};
use serde_json::{Value, json};
use std::path::PathBuf;
use urma_core::format::PublicRecord;
use urma_runtime::error::Error;
use urma_runtime::{node::Node, plan::PlanLimits};
use urma_wire::{Error as WireError, SyncError as WireSyncError, index::Index, reader::Reader};

#[derive(Args)]
pub(crate) struct PlanArgs {
    #[command(flatten)]
    access: VaultAccess,
    #[command(flatten)]
    node: NodeArgs,
    #[arg(help = "Public post text")]
    text: String,
    #[arg(long, default_value = "wire-plan.json")]
    output: PathBuf,
    #[arg(long, default_value_t = 1)]
    fee_rate: u64,
    #[arg(long, default_value_t = 100_000)]
    max_fee: u64,
}
#[derive(Args)]
pub(crate) struct PublishArgs {
    #[command(flatten)]
    node: NodeArgs,
    #[arg(long, default_value = "wire-plan.json")]
    plan: PathBuf,
    #[arg(short, long, help = "Approve the displayed exact plan and fee")]
    yes: bool,
    #[arg(long, default_value = "wire-progress.json")]
    journal: PathBuf,
}
#[derive(Args)]
pub(crate) struct IndexArgs {
    #[command(flatten)]
    node: NodeArgs,
    #[arg(long, default_value = "wire-index.json")]
    index: PathBuf,
    #[arg(long, default_value_t = 0)]
    start_height: u64,
    #[arg(long, default_value_t = 1000)]
    max_blocks: u64,
}
#[derive(Subcommand)]
pub(crate) enum ReadCommand {
    Feed {
        #[arg(long, default_value = "wire-index.json")]
        index: PathBuf,
        #[arg(long, default_value_t = 50)]
        limit: usize,
    },
    Record {
        #[arg(long, default_value = "wire-index.json")]
        index: PathBuf,
        txid: Txid,
    },
    Identity {
        #[arg(long, default_value = "wire-index.json")]
        index: PathBuf,
        author: XOnlyPublicKey,
    },
}

pub(crate) fn plan(args: PlanArgs) -> Result<Value, Error> {
    let node = args.node.connect()?;
    let vault = args.access.open()?;
    let signer = vault.keyring().active()?;
    let record = PublicRecord::Post(args.text);
    let plan = urma_runtime::plan::prepare_atomic(
        &node,
        &signer,
        &record,
        PlanLimits {
            fee_rate: args.fee_rate,
            max_fee: args.max_fee,
            max_records: 1,
        },
    )?;
    plan.save_new(&args.output)?;
    Ok(
        json!({"status":"prepared","plan":args.output,"plan_id":plan.id()?,"author":plan.author,"total_fee":plan.total_fee,"maximum_fee":plan.maximum_fee,"records":plan.records.len(),"broadcast":false}),
    )
}
pub(crate) fn publish(args: PublishArgs) -> Result<Value, Error> {
    publish_plan(
        &args.node,
        &args.plan,
        &args.journal,
        args.yes,
        "Publish public Wire text",
    )
}

pub(crate) fn index(args: IndexArgs) -> Result<Value, Error> {
    let reader = NodeReader(args.node.connect()?);
    Ok(serde_json::to_value(wire_sync_result(
        urma_wire::index::sync(&reader, &args.index, args.start_height, args.max_blocks),
    )?)?)
}
pub(crate) fn read(command: ReadCommand) -> Result<Value, Error> {
    match command {
        ReadCommand::Feed { index, limit } => {
            let index = wire_result(Index::load(&index))?;
            Ok(
                json!({"genesis":index.genesis,"records":wire_result(urma_wire::view::records(&index, limit))?,"start_height":index.start,"locally_cached":true}),
            )
        }
        ReadCommand::Record { index, txid } => wire_result(urma_wire::view::record(
            &wire_result(Index::load(&index))?,
            txid,
        )),
        ReadCommand::Identity { index, author } => wire_result(urma_wire::view::identity(
            &wire_result(Index::load(&index))?,
            author,
        )),
    }
}

fn wire_result<T>(result: Result<T, WireError>) -> Result<T, Error> {
    result.map_err(wire_error)
}

fn wire_error(cause: WireError) -> Error {
    match cause {
        WireError::Invalid(message) => Error::Invalid(message),
        WireError::Capacity(message) => Error::Capacity(message),
        WireError::Missing(message) => Error::Missing(message),
        WireError::Protocol(cause) => Error::Protocol(cause),
        WireError::Block(cause) => Error::from(cause),
        WireError::Io(cause) => Error::Io(cause),
        WireError::Json(cause) => Error::Json(cause),
        WireError::Hex(cause) => Error::Hex(cause),
        WireError::Integer(cause) => Error::Integer(cause),
        WireError::Hash(cause) => Error::Hash(cause),
        WireError::Secp256k1(cause) => Error::Secp256k1(cause),
        WireError::Persist(cause) => Error::Persist(cause),
        WireError::Context { message, cause } => Error::Context {
            message,
            cause: Box::new(wire_error(*cause)),
        },
    }
}

fn wire_sync_result<T>(result: Result<T, WireSyncError<Error>>) -> Result<T, Error> {
    match result {
        Ok(value) => Ok(value),
        Err(WireSyncError::Wire(cause)) => wire_result(Err(cause)),
        Err(WireSyncError::Source(cause)) => Err(cause),
    }
}

struct NodeReader(Node);
impl Reader for NodeReader {
    type Error = Error;

    fn genesis(&self) -> Result<BlockHash, Error> {
        Ok(self.0.chain().genesis()?.0)
    }
    fn tip_height(&self) -> Result<u64, Error> {
        self.0.tip_height()
    }
    fn block_hash(&self, height: u64) -> Result<BlockHash, Error> {
        self.0.block_hash(height)
    }
    fn block(&self, height: u64) -> Result<Block, Error> {
        self.0.block(height)
    }
    fn transaction(&self, txid: Txid) -> Result<Transaction, Error> {
        self.0.transaction(txid)
    }
}