ckb-cli 1.4.0

ckb command line interface
use std::collections::{HashMap, HashSet};
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};

use ckb_hash::{blake2b_256, new_blake2b};
use ckb_jsonrpc_types as rpc_types;
use ckb_sdk::{
    constants::{MIN_SECP_CELL_CAPACITY, ONE_CKB},
    traits::LiveCell,
    tx_builder::{BalanceTxCapacityError, TxBuilderError},
    util::serialize_signature,
    Address, AddressPayload, NetworkType, SECP256K1,
};
use ckb_signer::{KeyStore, ScryptType};
use ckb_types::{
    bytes::Bytes,
    core::{BlockView, Capacity, TransactionView},
    h256,
    packed::{CellInput, CellOutput, OutPoint},
    prelude::*,
    H160, H256,
};
use clap::ArgMatches;
use colored::Colorize;
use plugin_protocol::{CellIndex, LiveCellInfo};
use rpassword::prompt_password_stdout;

use super::arg_parser::{
    AddressParser, ArgParser, FixedHashParser, HexParser, PrivkeyWrapper, PubkeyHexParser,
};
use super::rpc::{AlertMessage, HttpRpcClient};
use super::tx_helper::SignerFn;
use crate::plugin::{KeyStoreHandler, SignTarget};
use crate::utils::genesis_info::GenesisInfo;

pub fn read_password(repeat: bool, prompt: Option<&str>) -> Result<String, String> {
    let prompt = prompt.unwrap_or("Password");
    let pass =
        prompt_password_stdout(format!("{}: ", prompt).as_str()).map_err(|err| err.to_string())?;
    if repeat {
        let repeat_pass =
            prompt_password_stdout("Repeat password: ").map_err(|err| err.to_string())?;
        if pass != repeat_pass {
            return Err("Passwords do not match".to_owned());
        }
    }
    Ok(pass)
}

pub fn get_key_store(ckb_cli_dir: PathBuf) -> Result<KeyStore, String> {
    let mut keystore_dir = ckb_cli_dir;
    keystore_dir.push("keystore");
    fs::create_dir_all(&keystore_dir)
        .map_err(|err| err.to_string())
        .and_then(|_| {
            KeyStore::from_dir(keystore_dir, ScryptType::default()).map_err(|err| err.to_string())
        })
}

pub fn get_address(network: Option<NetworkType>, m: &ArgMatches) -> Result<AddressPayload, String> {
    let address_opt: Option<Address> = AddressParser::new_sighash()
        .set_network_opt(network)
        .from_matches_opt(m, "address")?;
    let pubkey: Option<secp256k1::PublicKey> = PubkeyHexParser.from_matches_opt(m, "pubkey")?;
    let lock_arg: Option<H160> =
        FixedHashParser::<H160>::default().from_matches_opt(m, "lock-arg")?;
    let address = address_opt
        .map(|address| address.payload().clone())
        .or_else(|| pubkey.map(|pubkey| AddressPayload::from_pubkey(&pubkey)))
        .or_else(|| lock_arg.map(AddressPayload::from_pubkey_hash))
        .ok_or_else(|| "Please give one argument".to_owned())?;
    Ok(address)
}

pub fn get_signer(
    keystore: KeyStoreHandler,
    require_password: bool,
) -> impl Fn(&H160, &H256, &rpc_types::Transaction) -> Result<[u8; 65], String> + 'static {
    move |lock_arg: &H160, message: &H256, _tx: &rpc_types::Transaction| {
        let password = if require_password {
            let prompt = format!("Password for [{:x}]", lock_arg);
            Some(read_password(false, Some(prompt.as_str()))?)
        } else {
            None
        };
        let path = keystore.root_key_path(lock_arg.clone())?;
        let data = keystore.sign(
            lock_arg.clone(),
            &path,
            message.clone(),
            SignTarget::AnyMessage(message.clone()),
            password,
            true,
        )?;
        if data.len() != 65 {
            Err(format!(
                "Invalid signature data lenght: {}, data: {:?}",
                data.len(),
                data
            ))
        } else {
            let mut data_bytes = [0u8; 65];
            data_bytes.copy_from_slice(&data[..]);
            Ok(data_bytes)
        }
    }
}

pub fn check_alerts(rpc_client: &mut HttpRpcClient) {
    log::debug!("checking alerts...");
    if let Some(alerts) = rpc_client
        .get_blockchain_info()
        .ok()
        .map(|info| info.alerts)
    {
        for AlertMessage {
            id,
            priority,
            notice_until,
            message,
        } in alerts
        {
            if notice_until.0
                >= SystemTime::now()
                    .duration_since(UNIX_EPOCH)
                    .expect("Time went backwards")
                    .as_secs()
                    * 1000
            {
                eprintln!(
                    "[{}]: id={}, priority={}, message={}",
                    "alert".yellow().bold(),
                    id.to_string().blue().bold(),
                    priority.to_string().blue().bold(),
                    message.yellow().bold(),
                )
            }
        }
    }
}

pub fn get_genesis_info(
    genesis_info: &Option<GenesisInfo>,
    rpc_client: &mut HttpRpcClient,
) -> Result<GenesisInfo, String> {
    if let Some(genesis_info) = genesis_info {
        Ok(genesis_info.clone())
    } else {
        let genesis_block: BlockView = rpc_client
            .get_block_by_number(0)?
            .ok_or_else(|| String::from("Can not get genesis block"))?
            .into();
        GenesisInfo::from_block(&genesis_block)
    }
}

pub fn get_live_cell_with_cache(
    cache: &mut HashMap<(OutPoint, bool), (CellOutput, Bytes)>,
    client: &mut HttpRpcClient,
    out_point: OutPoint,
    with_data: bool,
) -> Result<(CellOutput, Bytes), String> {
    if let Some(output) = cache.get(&(out_point.clone(), with_data)).cloned() {
        Ok(output)
    } else {
        let output = get_live_cell(client, out_point.clone(), with_data)?;
        cache.insert((out_point, with_data), output.clone());
        Ok(output)
    }
}

pub fn get_live_cell(
    client: &mut HttpRpcClient,
    out_point: OutPoint,
    with_data: bool,
) -> Result<(CellOutput, Bytes), String> {
    let cell = client.get_live_cell(out_point.clone(), with_data)?;
    if cell.status != "live" {
        return Err(format!(
            "Invalid cell status: {}, out_point: {}",
            cell.status, out_point
        ));
    }
    let cell_status = cell.status.clone();
    cell.cell
        .map(|cell| {
            (
                cell.output.into(),
                cell.data
                    .map(|data| data.content.into_bytes())
                    .unwrap_or_default(),
            )
        })
        .ok_or_else(|| {
            format!(
                "Invalid input cell, status: {}, out_point: {}",
                cell_status, out_point
            )
        })
}

pub fn get_network_type(rpc_client: &mut HttpRpcClient) -> Result<NetworkType, String> {
    log::debug!("getting network type...");
    let chain_info = rpc_client.get_blockchain_info()?;
    NetworkType::from_raw_str(chain_info.chain.as_str())
        .ok_or_else(|| format!("Unexpected network type: {}", chain_info.chain))
}

pub fn check_capacity(capacity: u64, to_data_len: usize) -> Result<(), String> {
    if capacity < MIN_SECP_CELL_CAPACITY {
        return Err(format!(
            "Capacity can not less than {} shannons",
            MIN_SECP_CELL_CAPACITY
        ));
    }
    if capacity < MIN_SECP_CELL_CAPACITY + (to_data_len as u64 * ONE_CKB) {
        return Err(format!(
            "Capacity can not hold {} bytes of data",
            to_data_len
        ));
    }
    Ok(())
}

pub fn check_lack_of_capacity(transaction: &TransactionView) -> Result<(), String> {
    for (output, output_data) in transaction.outputs_with_data_iter() {
        let exact = output
            .clone()
            .as_builder()
            .build_exact_capacity(Capacity::bytes(output_data.len()).unwrap())
            .unwrap();
        let output_capacity: u64 = output.capacity().unpack();
        let exact_capacity: u64 = exact.capacity().unpack();
        if output_capacity < exact_capacity {
            return Err(format!(
                "Insufficient Cell Capacity, output_capacity({}) < exact_capacity({}), output: {}, output_data_size: {}",
                output_capacity,
                exact_capacity,
                output,
                output_data.len(),
            ));
        }
    }
    Ok(())
}

pub fn get_to_data(m: &ArgMatches) -> Result<Bytes, String> {
    let to_data_opt: Option<Bytes> = HexParser.from_matches_opt(m, "to-data")?;
    match to_data_opt {
        Some(data) => Ok(data),
        None => {
            if let Some(path) = m.value_of("to-data-path") {
                let mut content = Vec::new();
                let mut file = fs::File::open(path).map_err(|err| err.to_string())?;
                file.read_to_end(&mut content)
                    .map_err(|err| err.to_string())?;
                Ok(Bytes::from(content))
            } else {
                Ok(Bytes::new())
            }
        }
    }
}

pub fn get_privkey_signer(privkey: PrivkeyWrapper) -> SignerFn {
    let pubkey = secp256k1::PublicKey::from_secret_key(&SECP256K1, &privkey);
    let lock_arg = H160::from_slice(&blake2b_256(&pubkey.serialize()[..])[0..20])
        .expect("Generate hash(H160) from pubkey failed");
    Box::new(
        move |lock_args: &HashSet<H160>, message: &H256, _tx: &rpc_types::Transaction| {
            if lock_args.contains(&lock_arg) {
                if message == &h256!("0x0") {
                    Ok(Some([0u8; 65]))
                } else {
                    let message = secp256k1::Message::from_slice(message.as_bytes())
                        .expect("Convert to secp256k1 message failed");
                    let signature = SECP256K1.sign_ecdsa_recoverable(&message, &privkey);
                    Ok(Some(serialize_signature(&signature)))
                }
            } else {
                Ok(None)
            }
        },
    )
}

pub fn get_arg_value(matches: &ArgMatches, name: &str) -> Result<String, String> {
    matches
        .value_of(name)
        .map(|s| s.to_string())
        .ok_or_else(|| format!("<{}> is required", name))
}

pub fn to_live_cell_info(cell: &LiveCell) -> LiveCellInfo {
    let output_index: u32 = cell.out_point.index().unpack();
    LiveCellInfo {
        tx_hash: cell.out_point.tx_hash().unpack(),
        output_index,
        data_bytes: cell.output_data.len() as u64,
        lock_hash: cell.output.lock().calc_script_hash().unpack(),
        type_hashes: cell.output.type_().to_opt().map(|type_script| {
            (
                type_script.code_hash().unpack(),
                type_script.calc_script_hash().unpack(),
            )
        }),
        capacity: cell.output.capacity().unpack(),
        number: cell.block_number,
        index: CellIndex {
            tx_index: cell.tx_index,
            output_index,
        },
    }
}

pub fn address_json(payload: AddressPayload, is_new: bool) -> serde_json::Value {
    serde_json::json!({
        "mainnet": Address::new(NetworkType::Mainnet, payload.clone(), is_new).to_string(),
        "testnet": Address::new(NetworkType::Testnet, payload, is_new).to_string(),
    })
}

pub fn calculate_type_id(first_cell_input: &CellInput, output_index: u64) -> [u8; 32] {
    let mut blake2b = new_blake2b();
    blake2b.update(first_cell_input.as_slice());
    blake2b.update(&output_index.to_le_bytes());
    let mut ret = [0u8; 32];
    blake2b.finalize(&mut ret);
    ret
}

pub(crate) fn map_tx_builder_error_2_str(no_max_tx_fee: bool, err: TxBuilderError) -> String {
    if no_max_tx_fee {
        if let TxBuilderError::BalanceCapacity(BalanceTxCapacityError::CapacityNotEnough(ref msg)) =
            err
        {
            if msg.contains("can not create change cell") {
                return format!("{}, {}.", err, "try to calculate capacity again or try parameter `--max-tx-fee` to make small left capacity as transaction fee");
            }
        }
    }
    err.to_string()
}