/* This file is part of DarkFi (https://dark.fi)
 *
 * Copyright (C) 2020-2026 Dyne.org foundation
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */

use std::{
    io::{stdin, Read},
    process::exit,
    str::FromStr,
};

use prettytable::{format, row, Table};
use rand::rngs::OsRng;
use smol::{channel::unbounded, stream::StreamExt};
use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
use tracing::info;
use tracing_appender::non_blocking;
use url::Url;

use darkfi::{
    async_daemonize, cli_desc,
    system::ExecutorPtr,
    util::{
        encoding::base64,
        logger::{set_terminal_writer, ChannelWriter},
        parse::{decode_base10, encode_base10},
        path::expand_path,
    },
    zk::halo2::Field,
    Error, Result,
};
use darkfi_dao_contract::{blockwindow, model::DaoProposalBulla, DaoFunction};
use darkfi_money_contract::model::{Coin, CoinAttributes, TokenId};
use darkfi_sdk::{
    crypto::{
        keypair::{Address, Keypair, Network, SecretKey, StandardAddress},
        note::AeadEncryptedNote,
        BaseBlind, ContractId, FuncId, FuncRef, DAO_CONTRACT_ID,
    },
    pasta::{group::ff::PrimeField, pallas},
    tx::TransactionHash,
};
use darkfi_serial::{deserialize_async, serialize_async};

use drk::{
    cli_util::{
        display_mining_config, generate_completions, kaching, parse_blockchain_config,
        parse_calls_from_stdin, parse_mining_config_from_stdin, parse_token_pair, parse_tree,
        parse_tx_from_stdin, parse_value_pair, print_output, tx_from_calls_mapped,
    },
    common::*,
    dao::{DaoParams, ProposalRecord},
    interactive::interactive,
    money::BALANCE_BASE10_DECIMALS,
    swap::PartialSwapData,
    Drk,
};

const CONFIG_FILE: &str = "drk_config.toml";
const CONFIG_FILE_CONTENTS: &str = include_str!("../drk_config.toml");

// Dev Note: when adding/modifying args here,
// don't forget to update cli_util::generate_completions()
// and interactive::help().
#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
#[serde(default)]
#[structopt(name = "drk", about = cli_desc!())]
struct Args {
    #[structopt(short, long)]
    /// Configuration file to use
    config: Option<String>,

    #[structopt(short, long, default_value = "testnet")]
    /// Blockchain network to use
    network: String,

    #[structopt(subcommand)]
    /// Sub command to execute
    command: Subcmd,

    #[structopt(short, long)]
    /// Flag indicating whether you want some fun in your life
    fun: bool,

    #[structopt(short, long)]
    /// Set log file to ouput into
    log: Option<String>,

    #[structopt(short, parse(from_occurrences))]
    /// Increase verbosity (-vvv supported)
    verbose: u8,
}

// Dev Note: when adding/modifying commands here,
// don't forget to update cli_util::generate_completions()
#[derive(Clone, Debug, Deserialize, StructOpt)]
enum Subcmd {
    /// Enter Drk interactive shell
    Interactive,

    /// Fun
    Kaching,

    /// Send a ping request to the darkfid RPC endpoint
    Ping,

    /// Generate a SHELL completion script and print to stdout
    Completions {
        /// The Shell you want to generate script for
        shell: String,
    },

    /// Wallet operations
    Wallet {
        #[structopt(subcommand)]
        /// Sub command to execute
        command: WalletSubcmd,
    },

    /// Read a transaction from stdin and mark its input coins as spent
    Spend,

    /// Unspend a coin
    Unspend {
        /// base64-encoded coin to mark as unspent
        coin: String,
    },

    /// Create a payment transaction
    Transfer {
        /// Amount to send
        amount: String,

        /// Token ID to send
        token: String,

        /// Recipient address
        recipient: String,

        /// Optional contract spend hook to use
        spend_hook: Option<String>,

        /// Optional user data to use
        user_data: Option<String>,

        #[structopt(long)]
        /// Split the output coin into two equal halves
        half_split: bool,
    },

    /// OTC atomic swap
    Otc {
        #[structopt(subcommand)]
        /// Sub command to execute
        command: OtcSubcmd,
    },

    /// DAO functionalities
    Dao {
        #[structopt(subcommand)]
        /// Sub command to execute
        command: DaoSubcmd,
    },

    /// Attach the fee call to a transaction given from stdin
    AttachFee,

    /// Create a transaction from newline-separated calls from stdin and attach the fee call
    TxFromCalls {
        /// Optional parent/children dependency map for the calls
        calls_map: Option<String>,
    },

    /// Inspect a transaction from stdin
    Inspect,

    /// Read a transaction from stdin and broadcast it
    Broadcast,

    /// Scan the blockchain and parse relevant transactions
    Scan {
        #[structopt(long)]
        /// Reset wallet state to provided block height and start scanning
        reset: Option<u32>,
    },

    /// Explorer related subcommands
    Explorer {
        #[structopt(subcommand)]
        /// Sub command to execute
        command: ExplorerSubcmd,
    },

    /// Manage Token aliases
    Alias {
        #[structopt(subcommand)]
        /// Sub command to execute
        command: AliasSubcmd,
    },

    /// Token functionalities
    Token {
        #[structopt(subcommand)]
        /// Sub command to execute
        command: TokenSubcmd,
    },

    /// Contract functionalities
    Contract {
        #[structopt(subcommand)]
        /// Sub command to execute
        command: ContractSubcmd,
    },
}

#[derive(Clone, Debug, Deserialize, StructOpt)]
enum WalletSubcmd {
    /// Initialize wallet database
    Initialize,

    /// Generate a new keypair in the wallet
    Keygen,

    /// Query the wallet for known balances
    Balance,

    /// Get the default address in the wallet
    Address,

    /// Print all the addresses in the wallet
    Addresses,

    /// Set the default address in the wallet
    DefaultAddress {
        /// Identifier of the address
        index: usize,
    },

    /// Print all the secret keys from the wallet
    Secrets,

    /// Import secret keys from stdin into the wallet, separated by newlines
    ImportSecrets,

    /// Print the Merkle tree in the wallet
    Tree,

    /// Print all the coins in the wallet
    Coins,

    /// Print a wallet address mining configuration
    MiningConfig {
        /// Identifier of the address
        index: usize,

        /// Optional contract spend hook to use
        spend_hook: Option<String>,

        /// Optional user data to use
        user_data: Option<String>,
    },
}

#[derive(Clone, Debug, Deserialize, StructOpt)]
enum OtcSubcmd {
    /// Initialize the first half of the atomic swap
    Init {
        /// Value pair to send:recv (11.55:99.42)
        #[structopt(short, long)]
        value_pair: String,

        /// Token pair to send:recv (f00:b4r)
        #[structopt(short, long)]
        token_pair: String,
    },

    /// Build entire swap tx given the first half from stdin
    Join,

    /// Inspect a swap half or the full swap tx from stdin
    Inspect,

    /// Sign a swap transaction given from stdin
    Sign,
}

#[derive(Clone, Debug, Deserialize, StructOpt)]
enum DaoSubcmd {
    /// Create DAO parameters
    Create {
        /// The minimum amount of governance tokens needed to open a proposal for this DAO
        proposer_limit: String,
        /// Minimal threshold of participating total tokens needed for a proposal to pass
        quorum: String,
        /// Minimal threshold of participating total tokens needed for a proposal to
        /// be considered strongly supported, enabling early execution.
        /// Must be greater than or equal to normal quorum.
        early_exec_quorum: String,
        /// The ratio of yes votes/total votes needed for a proposal to pass (2 decimals)
        approval_ratio: f64,
        /// DAO's governance token ID
        gov_token_id: String,
    },

    /// View DAO data from stdin
    View,

    /// Import DAO data from stdin
    Import {
        /// Name identifier for the DAO
        name: String,
    },

    /// Remove a DAO and all its data
    Remove {
        /// Name identifier for the DAO
        name: String,
    },

    /// List imported DAOs (or info about a specific one)
    List {
        /// Name identifier for the DAO (optional)
        name: Option<String>,
    },

    /// Show the balance of a DAO
    Balance {
        /// Name identifier for the DAO
        name: String,
    },

    /// Mint an imported DAO on-chain
    Mint {
        /// Name identifier for the DAO
        name: String,
    },

    /// Create a transfer proposal for a DAO
    ProposeTransfer {
        /// Name identifier for the DAO
        name: String,

        /// Duration of the proposal, in block windows
        duration: u64,

        /// Amount to send
        amount: String,

        /// Token ID to send
        token: String,

        /// Recipient address
        recipient: String,

        /// Optional contract spend hook to use
        spend_hook: Option<String>,

        /// Optional user data to use
        user_data: Option<String>,
    },

    /// Create a generic proposal for a DAO
    ProposeGeneric {
        /// Name identifier for the DAO
        name: String,

        /// Duration of the proposal, in block windows
        duration: u64,

        /// Optional user data to use
        user_data: Option<String>,
    },

    /// List DAO proposals
    Proposals {
        /// Name identifier for the DAO
        name: String,
    },

    /// View a DAO proposal data
    Proposal {
        /// Bulla identifier for the proposal
        bulla: String,

        #[structopt(long)]
        /// Encrypt the proposal and encode it to base64
        export: bool,

        #[structopt(long)]
        /// Create the proposal transaction
        mint_proposal: bool,
    },

    /// Import a base64 encoded and encrypted proposal from stdin
    ProposalImport,

    /// Vote on a given proposal
    Vote {
        /// Bulla identifier for the proposal
        bulla: String,

        /// Vote (0 for NO, 1 for YES)
        vote: u8,

        /// Optional vote weight (amount of governance tokens)
        vote_weight: Option<String>,
    },

    /// Execute a DAO proposal
    Exec {
        /// Bulla identifier for the proposal
        bulla: String,

        #[structopt(long)]
        /// Execute the proposal early
        early: bool,
    },

    /// Print the DAO contract base64-encoded spend hook
    SpendHook,

    /// Print a DAO mining configuration
    MiningConfig {
        /// Name identifier for the DAO
        name: String,
    },
}

#[derive(Clone, Debug, Deserialize, StructOpt)]
enum ExplorerSubcmd {
    /// Fetch a blockchain transaction by hash
    FetchTx {
        /// Transaction hash
        tx_hash: String,

        #[structopt(long)]
        /// Encode transaction to base64
        encode: bool,
    },

    /// Read a transaction from stdin and simulate it
    SimulateTx,

    /// Fetch broadcasted transactions history
    TxsHistory {
        /// Fetch specific history record (optional)
        tx_hash: Option<String>,

        #[structopt(long)]
        /// Encode specific history record transaction to base64
        encode: bool,
    },

    /// Remove reverted transactions from history
    ClearReverted,

    /// Fetch scanned blocks records
    ScannedBlocks {
        /// Fetch specific height record (optional)
        height: Option<u32>,
    },

    /// Read a mining configuration from stdin and display its parts
    MiningConfig,
}

#[derive(Clone, Debug, Deserialize, StructOpt)]
enum AliasSubcmd {
    /// Create a Token alias
    Add {
        /// Token alias
        alias: String,

        /// Token to create alias for
        token: String,
    },

    /// Print alias info of optional arguments.
    /// If no argument is provided, list all the aliases in the wallet.
    Show {
        /// Token alias to search for
        #[structopt(short, long)]
        alias: Option<String>,

        /// Token to search alias for
        #[structopt(short, long)]
        token: Option<String>,
    },

    /// Remove a Token alias
    Remove {
        /// Token alias to remove
        alias: String,
    },
}

#[derive(Clone, Debug, Deserialize, StructOpt)]
enum TokenSubcmd {
    /// Import a mint authority
    Import {
        /// Mint authority secret key
        secret_key: String,

        /// Mint authority token blind
        token_blind: String,
    },

    /// Generate a new mint authority
    GenerateMint,

    /// List token IDs with available mint authorities
    List,

    /// Mint tokens
    Mint {
        /// Token ID to mint
        token: String,

        /// Amount to mint
        amount: String,

        /// Recipient of the minted tokens
        recipient: String,

        /// Optional contract spend hook to use
        spend_hook: Option<String>,

        /// Optional user data to use
        user_data: Option<String>,
    },

    /// Freeze a token mint
    Freeze {
        /// Token ID to freeze
        token: String,
    },
}

#[derive(Clone, Debug, Deserialize, StructOpt)]
enum ContractSubcmd {
    /// Generate a new deploy authority
    GenerateDeploy,

    /// List deploy authorities in the wallet (or a specific one)
    List {
        /// Contract ID (optional)
        contract_id: Option<String>,
    },

    /// Export a contract history record wasm bincode and deployment instruction, encoded to base64
    ExportData {
        /// Record transaction hash
        tx_hash: String,
    },

    /// Deploy a smart contract
    Deploy {
        /// Contract ID (deploy authority)
        deploy_auth: String,

        /// Path to contract wasm bincode
        wasm_path: String,

        /// Optional path to serialized deploy instruction
        deploy_ix: Option<String>,
    },

    /// Lock a smart contract
    Lock {
        /// Contract ID (deploy authority)
        deploy_auth: String,
    },
}

/// Auxiliary function to create a `Drk` wallet for provided configuration.
async fn new_wallet(
    network: Network,
    cache_path: String,
    wallet_path: String,
    wallet_pass: String,
    endpoint: Option<Url>,
    ex: &ExecutorPtr,
    fun: bool,
) -> Drk {
    // Script kiddies protection
    if wallet_pass == "changeme" {
        eprintln!("Please don't use default wallet password...");
        exit(2);
    }

    match Drk::new(network, cache_path, wallet_path, wallet_pass, endpoint, ex, fun).await {
        Ok(wallet) => wallet,
        Err(e) => {
            eprintln!("Error initializing wallet: {e}");
            exit(2);
        }
    }
}

async_daemonize!(realmain);
async fn realmain(args: Args, ex: ExecutorPtr) -> Result<()> {
    // Grab blockchain network configuration
    let (network, blockchain_config) = match args.network.as_str() {
        "localnet" => parse_blockchain_config(args.config, "localnet", CONFIG_FILE).await?,
        "testnet" => parse_blockchain_config(args.config, "testnet", CONFIG_FILE).await?,
        "mainnet" => parse_blockchain_config(args.config, "mainnet", CONFIG_FILE).await?,
        _ => {
            eprintln!("Unsupported chain `{}`", args.network);
            return Err(Error::UnsupportedChain)
        }
    };

    match args.command {
        Subcmd::Interactive => {
            // Create an unbounded smol channel, so we can have a
            // printing queue the background logger and tasks can
            // submit messages to so the shell prints them.
            let (shell_sender, shell_receiver) = unbounded();

            // Set the logging writer
            let (non_blocking, _guard) =
                non_blocking(ChannelWriter { sender: shell_sender.clone() });
            set_terminal_writer(args.verbose, non_blocking)?;

            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                Some(blockchain_config.endpoint.clone()),
                &ex,
                args.fun,
            )
            .await
            .into_ptr();

            interactive(
                &drk,
                &blockchain_config.endpoint,
                &blockchain_config.history_path,
                &shell_sender,
                &shell_receiver,
                &ex,
            )
            .await;

            drk.read().await.stop_rpc_client().await?;
            Ok(())
        }

        Subcmd::Kaching => {
            if !args.fun {
                println!("Apparently you don't like fun...");
                return Ok(())
            }
            kaching().await;
            Ok(())
        }

        Subcmd::Ping => {
            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                Some(blockchain_config.endpoint),
                &ex,
                args.fun,
            )
            .await;
            let mut output = vec![];
            if let Err(e) = drk.ping(&mut output).await {
                print_output(&output);
                return Err(e)
            };
            print_output(&output);
            drk.stop_rpc_client().await
        }

        Subcmd::Completions { shell } => {
            println!("{}", generate_completions(&shell)?);
            Ok(())
        }

        Subcmd::Wallet { command } => {
            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                None,
                &ex,
                args.fun,
            )
            .await;

            match command {
                WalletSubcmd::Initialize => {
                    if let Err(e) = drk.initialize_wallet().await {
                        eprintln!("Error initializing wallet: {e}");
                        exit(2);
                    }
                    let mut output = vec![];
                    if let Err(e) = drk.initialize_money(&mut output).await {
                        print_output(&output);
                        eprintln!("Failed to initialize Money: {e}");
                        exit(2);
                    }
                    print_output(&output);
                    if let Err(e) = drk.initialize_dao().await {
                        eprintln!("Failed to initialize DAO: {e}");
                        exit(2);
                    }
                    if let Err(e) = drk.initialize_deployooor() {
                        eprintln!("Failed to initialize Deployooor: {e}");
                        exit(2);
                    }
                }

                WalletSubcmd::Keygen => {
                    let mut output = vec![];
                    if let Err(e) = drk.money_keygen(&mut output).await {
                        print_output(&output);
                        eprintln!("Failed to generate keypair: {e}");
                        exit(2);
                    }
                    print_output(&output);
                }

                WalletSubcmd::Balance => {
                    let balmap = drk.money_balance().await?;

                    let aliases_map = drk.get_aliases_mapped_by_token().await?;

                    // Create a prettytable with the new data:
                    let mut table = Table::new();
                    table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
                    table.set_titles(row!["Token ID", "Aliases", "Balance"]);
                    for (token_id, balance) in balmap.iter() {
                        let aliases = match aliases_map.get(token_id) {
                            Some(a) => a,
                            None => "-",
                        };

                        table.add_row(row![
                            token_id,
                            aliases,
                            encode_base10(*balance, BALANCE_BASE10_DECIMALS)
                        ]);
                    }

                    if table.is_empty() {
                        println!("No unspent balances found");
                    } else {
                        println!("{table}");
                    }
                }

                WalletSubcmd::Address => match drk.default_address().await {
                    Ok(public_key) => {
                        let address: Address =
                            StandardAddress::from_public(drk.network, public_key).into();
                        println!("{address}");
                    }
                    Err(e) => {
                        eprintln!("Failed to fetch default address: {e}");
                        exit(2);
                    }
                },

                WalletSubcmd::Addresses => {
                    let addresses = drk.addresses().await?;
                    let table = prettytable_addrs(drk.network, &addresses);

                    if table.is_empty() {
                        println!("No addresses found");
                    } else {
                        println!("{table}");
                    }
                }

                WalletSubcmd::DefaultAddress { index } => {
                    if let Err(e) = drk.set_default_address(index) {
                        eprintln!("Failed to set default address: {e}");
                        exit(2);
                    }
                }

                WalletSubcmd::Secrets => {
                    for secret in drk.get_money_secrets().await? {
                        println!("{secret}");
                    }
                }

                WalletSubcmd::ImportSecrets => {
                    let mut secrets = vec![];
                    let lines = stdin().lines();
                    for (i, line) in lines.enumerate() {
                        if let Ok(line) = line {
                            let bytes = bs58::decode(&line.trim()).into_vec()?;
                            let Ok(secret) = deserialize_async(&bytes).await else {
                                println!("Warning: Failed to deserialize secret on line {i}");
                                continue
                            };
                            secrets.push(secret);
                        }
                    }

                    let mut output = vec![];
                    let pubkeys = match drk.import_money_secrets(secrets, &mut output).await {
                        Ok(p) => {
                            print_output(&output);
                            p
                        }
                        Err(e) => {
                            print_output(&output);
                            eprintln!("Failed to import secret keys into wallet: {e}");
                            exit(2);
                        }
                    };

                    for key in pubkeys {
                        println!("{key}");
                    }
                }

                WalletSubcmd::Tree => {
                    println!("{:#?}", drk.get_money_tree().await?);
                }

                WalletSubcmd::Coins => {
                    let coins = drk.get_coins(true).await?;
                    if coins.is_empty() {
                        return Ok(())
                    }
                    let aliases_map = drk.get_aliases_mapped_by_token().await?;
                    let table = prettytable_coins(&coins, &aliases_map);
                    println!("{table}");
                }

                WalletSubcmd::MiningConfig { index, spend_hook, user_data } => {
                    let spend_hook = match spend_hook {
                        Some(s) => match FuncId::from_str(&s) {
                            Ok(s) => Some(s),
                            Err(e) => {
                                eprintln!("Invalid spend hook: {e}");
                                exit(2);
                            }
                        },
                        None => None,
                    };

                    let user_data = match user_data {
                        Some(u) => {
                            let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
                                Ok(b) => b,
                                Err(e) => {
                                    eprintln!("Invalid user data: {e:?}");
                                    exit(2);
                                }
                            };

                            match pallas::Base::from_repr(bytes).into() {
                                Some(v) => Some(v),
                                None => {
                                    eprintln!("Invalid user data");
                                    exit(2);
                                }
                            }
                        }
                        None => None,
                    };

                    let mut output = vec![];
                    if let Err(e) =
                        drk.mining_config(index, spend_hook, user_data, &mut output).await
                    {
                        print_output(&output);
                        eprintln!("Failed to generate wallet mining configuration: {e}");
                        exit(2);
                    }
                    print_output(&output);
                }
            }

            Ok(())
        }

        Subcmd::Spend => {
            let tx = parse_tx_from_stdin().await?;

            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                None,
                &ex,
                args.fun,
            )
            .await;

            let mut output = vec![];
            if let Err(e) = drk.mark_tx_spend(&tx, &mut output).await {
                print_output(&output);
                eprintln!("Failed to mark transaction coins as spent: {e}");
                exit(2);
            };
            print_output(&output);

            Ok(())
        }

        Subcmd::Unspend { coin } => {
            let bytes: [u8; 32] = match bs58::decode(&coin).into_vec()?.try_into() {
                Ok(b) => b,
                Err(e) => {
                    eprintln!("Invalid coin: {e:?}");
                    exit(2);
                }
            };

            let elem: pallas::Base = match pallas::Base::from_repr(bytes).into() {
                Some(v) => v,
                None => {
                    eprintln!("Invalid coin");
                    exit(2);
                }
            };

            let coin = Coin::from(elem);
            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                None,
                &ex,
                args.fun,
            )
            .await;
            if let Err(e) = drk.unspend_coin(&coin).await {
                eprintln!("Failed to mark coin as unspent: {e}");
                exit(2);
            }

            Ok(())
        }

        Subcmd::Transfer { amount, token, recipient, spend_hook, user_data, half_split } => {
            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                Some(blockchain_config.endpoint),
                &ex,
                args.fun,
            )
            .await;

            if let Err(e) = f64::from_str(&amount) {
                eprintln!("Invalid amount: {e}");
                exit(2);
            }

            let rcpt = match Address::from_str(&recipient) {
                Ok(r) => r,
                Err(e) => {
                    eprintln!("Invalid recipient: {e}");
                    exit(2);
                }
            };

            if rcpt.network() != drk.network {
                eprintln!("Recipient address prefix mismatch");
                exit(2);
            }

            let token_id = match drk.get_token(token).await {
                Ok(t) => t,
                Err(e) => {
                    eprintln!("Invalid token alias: {e}");
                    exit(2);
                }
            };

            let spend_hook = match spend_hook {
                Some(s) => match FuncId::from_str(&s) {
                    Ok(s) => Some(s),
                    Err(e) => {
                        eprintln!("Invalid spend hook: {e}");
                        exit(2);
                    }
                },
                None => None,
            };

            let user_data = match user_data {
                Some(u) => {
                    let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
                        Ok(b) => b,
                        Err(e) => {
                            eprintln!("Invalid user data: {e:?}");
                            exit(2);
                        }
                    };

                    match pallas::Base::from_repr(bytes).into() {
                        Some(v) => Some(v),
                        None => {
                            eprintln!("Invalid user data");
                            exit(2);
                        }
                    }
                }
                None => None,
            };

            let tx = match drk
                .transfer(&amount, token_id, *rcpt.public_key(), spend_hook, user_data, half_split)
                .await
            {
                Ok(t) => t,
                Err(e) => {
                    eprintln!("Failed to create payment transaction: {e}");
                    exit(2);
                }
            };

            println!("{}", base64::encode(&serialize_async(&tx).await));

            drk.stop_rpc_client().await
        }

        Subcmd::Otc { command } => match command {
            OtcSubcmd::Init { value_pair, token_pair } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;
                let value_pair = parse_value_pair(&value_pair)?;
                let token_pair = parse_token_pair(&drk, &token_pair).await?;

                let half = match drk.init_swap(value_pair, token_pair, None, None, None).await {
                    Ok(h) => h,
                    Err(e) => {
                        eprintln!("Failed to create swap transaction half: {e}");
                        exit(2);
                    }
                };

                println!("{}", base64::encode(&serialize_async(&half).await));
                drk.stop_rpc_client().await
            }

            OtcSubcmd::Join => {
                let mut buf = String::new();
                stdin().read_to_string(&mut buf)?;
                let Some(bytes) = base64::decode(buf.trim()) else {
                    eprintln!("Failed to decode partial swap data");
                    exit(2);
                };

                let partial: PartialSwapData = deserialize_async(&bytes).await?;

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;
                let tx = match drk.join_swap(partial, None, None, None).await {
                    Ok(tx) => tx,
                    Err(e) => {
                        eprintln!("Failed to create a join swap transaction: {e}");
                        exit(2);
                    }
                };

                println!("{}", base64::encode(&serialize_async(&tx).await));
                drk.stop_rpc_client().await
            }

            OtcSubcmd::Inspect => {
                let mut buf = String::new();
                stdin().read_to_string(&mut buf)?;
                let Some(bytes) = base64::decode(buf.trim()) else {
                    eprintln!("Failed to decode swap transaction");
                    exit(2);
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let mut output = vec![];
                if let Err(e) = drk.inspect_swap(bytes, &mut output).await {
                    print_output(&output);
                    eprintln!("Failed to inspect swap: {e}");
                    exit(2);
                };
                print_output(&output);

                Ok(())
            }

            OtcSubcmd::Sign => {
                let mut tx = parse_tx_from_stdin().await?;

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                if let Err(e) = drk.sign_swap(&mut tx).await {
                    eprintln!("Failed to sign joined swap transaction: {e}");
                    exit(2);
                };

                println!("{}", base64::encode(&serialize_async(&tx).await));
                Ok(())
            }
        },

        Subcmd::Dao { command } => match command {
            DaoSubcmd::Create {
                proposer_limit,
                quorum,
                early_exec_quorum,
                approval_ratio,
                gov_token_id,
            } => {
                if let Err(e) = f64::from_str(&proposer_limit) {
                    eprintln!("Invalid proposer limit: {e}");
                    exit(2);
                }
                if let Err(e) = f64::from_str(&quorum) {
                    eprintln!("Invalid quorum: {e}");
                    exit(2);
                }
                if let Err(e) = f64::from_str(&early_exec_quorum) {
                    eprintln!("Invalid early exec quorum: {e}");
                    exit(2);
                }

                let proposer_limit = decode_base10(&proposer_limit, BALANCE_BASE10_DECIMALS, true)?;
                let quorum = decode_base10(&quorum, BALANCE_BASE10_DECIMALS, true)?;
                let early_exec_quorum =
                    decode_base10(&early_exec_quorum, BALANCE_BASE10_DECIMALS, true)?;

                if approval_ratio > 1.0 {
                    eprintln!("Error: Approval ratio cannot be >1.0");
                    exit(2);
                }

                let approval_ratio_base = 100_u64;
                let approval_ratio_quot = (approval_ratio * approval_ratio_base as f64) as u64;

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let gov_token_id = match drk.get_token(gov_token_id).await {
                    Ok(g) => g,
                    Err(e) => {
                        eprintln!("Invalid Token ID: {e}");
                        exit(2);
                    }
                };

                let notes_keypair = Keypair::random(&mut OsRng);
                let proposer_keypair = Keypair::random(&mut OsRng);
                let proposals_keypair = Keypair::random(&mut OsRng);
                let votes_keypair = Keypair::random(&mut OsRng);
                let exec_keypair = Keypair::random(&mut OsRng);
                let early_exec_keypair = Keypair::random(&mut OsRng);
                let bulla_blind = BaseBlind::random(&mut OsRng);

                let params = DaoParams::new(
                    proposer_limit,
                    quorum,
                    early_exec_quorum,
                    approval_ratio_base,
                    approval_ratio_quot,
                    gov_token_id,
                    Some(notes_keypair.secret),
                    notes_keypair.public,
                    Some(proposer_keypair.secret),
                    proposer_keypair.public,
                    Some(proposals_keypair.secret),
                    proposals_keypair.public,
                    Some(votes_keypair.secret),
                    votes_keypair.public,
                    Some(exec_keypair.secret),
                    exec_keypair.public,
                    Some(early_exec_keypair.secret),
                    early_exec_keypair.public,
                    bulla_blind,
                );

                println!("{}", params.toml_str());

                Ok(())
            }

            DaoSubcmd::View => {
                let mut buf = String::new();
                stdin().read_to_string(&mut buf)?;
                let params = DaoParams::from_toml_str(&buf)?;
                println!("{params}");

                Ok(())
            }

            DaoSubcmd::Import { name } => {
                let mut buf = String::new();
                stdin().read_to_string(&mut buf)?;
                let params = DaoParams::from_toml_str(&buf)?;

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let mut output = vec![];
                if let Err(e) = drk.import_dao(&name, &params, &mut output).await {
                    print_output(&output);
                    eprintln!("Failed to import DAO: {e}");
                    exit(2);
                }
                print_output(&output);

                Ok(())
            }

            DaoSubcmd::Remove { name } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let mut output = vec![];
                if let Err(e) = drk.remove_dao(&name, &mut output).await {
                    print_output(&output);
                    eprintln!("Failed to remove DAO: {e}");
                    exit(2);
                }
                print_output(&output);

                Ok(())
            }

            DaoSubcmd::List { name } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let mut output = vec![];
                if let Err(e) = drk.dao_list(&name, &mut output).await {
                    print_output(&output);
                    eprintln!("Failed to list DAO: {e}");
                    exit(2);
                }
                print_output(&output);

                Ok(())
            }

            DaoSubcmd::Balance { name } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let balmap = match drk.dao_balance(&name).await {
                    Ok(b) => b,
                    Err(e) => {
                        eprintln!("Failed to fetch DAO balance: {e}");
                        exit(2);
                    }
                };

                let aliases_map = match drk.get_aliases_mapped_by_token().await {
                    Ok(a) => a,
                    Err(e) => {
                        eprintln!("Failed to fetch wallet aliases: {e}");
                        exit(2);
                    }
                };

                let table = prettytable_balance(&balmap, &aliases_map);

                if table.is_empty() {
                    println!("No unspent balances found");
                } else {
                    println!("{table}");
                }

                Ok(())
            }

            DaoSubcmd::Mint { name } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;
                let tx = match drk.dao_mint(&name).await {
                    Ok(tx) => tx,
                    Err(e) => {
                        eprintln!("Failed to mint DAO: {e}");
                        exit(2);
                    }
                };

                println!("{}", base64::encode(&serialize_async(&tx).await));
                drk.stop_rpc_client().await
            }

            DaoSubcmd::ProposeTransfer {
                name,
                duration,
                amount,
                token,
                recipient,
                spend_hook,
                user_data,
            } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;

                if let Err(e) = f64::from_str(&amount) {
                    eprintln!("Invalid amount: {e}");
                    exit(2);
                }

                let rcpt = match Address::from_str(&recipient) {
                    Ok(r) => r,
                    Err(e) => {
                        eprintln!("Invalid recipient: {e}");
                        exit(2);
                    }
                };

                if rcpt.network() != drk.network {
                    eprintln!("Recipient address prefix mismatch");
                    exit(2);
                }

                let token_id = match drk.get_token(token).await {
                    Ok(t) => t,
                    Err(e) => {
                        eprintln!("Invalid token alias: {e}");
                        exit(2);
                    }
                };

                let spend_hook = match spend_hook {
                    Some(s) => match FuncId::from_str(&s) {
                        Ok(s) => Some(s),
                        Err(e) => {
                            eprintln!("Invalid spend hook: {e}");
                            exit(2);
                        }
                    },
                    None => None,
                };

                let user_data = match user_data {
                    Some(u) => {
                        let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
                            Ok(b) => b,
                            Err(e) => {
                                eprintln!("Invalid user data: {e:?}");
                                exit(2);
                            }
                        };

                        match pallas::Base::from_repr(bytes).into() {
                            Some(v) => Some(v),
                            None => {
                                eprintln!("Invalid user data");
                                exit(2);
                            }
                        }
                    }
                    None => None,
                };

                let proposal = match drk
                    .dao_propose_transfer(
                        &name,
                        duration,
                        &amount,
                        token_id,
                        *rcpt.public_key(),
                        spend_hook,
                        user_data,
                    )
                    .await
                {
                    Ok(p) => p,
                    Err(e) => {
                        eprintln!("Failed to create DAO transfer proposal: {e}");
                        exit(2);
                    }
                };

                println!("Generated proposal: {}", proposal.bulla());

                drk.stop_rpc_client().await
            }

            DaoSubcmd::ProposeGeneric { name, duration, user_data } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;

                let user_data = match user_data {
                    Some(u) => {
                        let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
                            Ok(b) => b,
                            Err(e) => {
                                eprintln!("Invalid user data: {e:?}");
                                exit(2);
                            }
                        };

                        match pallas::Base::from_repr(bytes).into() {
                            Some(v) => Some(v),
                            None => {
                                eprintln!("Invalid user data");
                                exit(2);
                            }
                        }
                    }
                    None => None,
                };

                let proposal = match drk.dao_propose_generic(&name, duration, user_data).await {
                    Ok(p) => p,
                    Err(e) => {
                        eprintln!("Failed to create DAO generic proposal: {e}");
                        exit(2);
                    }
                };

                println!("Generated proposal: {}", proposal.bulla());

                drk.stop_rpc_client().await
            }

            DaoSubcmd::Proposals { name } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let proposals = drk.get_dao_proposals(&name).await?;

                for (i, proposal) in proposals.iter().enumerate() {
                    println!("{i}. {}", proposal.bulla());
                }

                Ok(())
            }

            DaoSubcmd::Proposal { bulla, export, mint_proposal } => {
                let bulla = match DaoProposalBulla::from_str(&bulla) {
                    Ok(b) => b,
                    Err(e) => {
                        eprintln!("Invalid proposal bulla: {e}");
                        exit(2);
                    }
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;
                let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?;

                if export {
                    // Retrieve the DAO
                    let dao = drk.get_dao_by_bulla(&proposal.proposal.dao_bulla).await?;

                    // Encypt the proposal
                    let enc_note = AeadEncryptedNote::encrypt(
                        &proposal,
                        &dao.params.dao.proposals_public_key,
                        &mut OsRng,
                    )
                    .unwrap();

                    // Export it to base64
                    println!("{}", base64::encode(&serialize_async(&enc_note).await));
                    return drk.stop_rpc_client().await
                }

                if mint_proposal {
                    // Identify proposal type by its auth calls
                    for call in &proposal.proposal.auth_calls {
                        // We only support transfer right now
                        if call.function_code == DaoFunction::AuthMoneyTransfer as u8 {
                            let tx = match drk.dao_transfer_proposal_tx(&proposal).await {
                                Ok(tx) => tx,
                                Err(e) => {
                                    eprintln!("Failed to create DAO transfer proposal: {e}");
                                    exit(2);
                                }
                            };

                            println!("{}", base64::encode(&serialize_async(&tx).await));
                            return drk.stop_rpc_client().await
                        }
                    }

                    // If proposal has no auth calls, we consider it a generic one
                    if proposal.proposal.auth_calls.is_empty() {
                        let tx = match drk.dao_generic_proposal_tx(&proposal).await {
                            Ok(tx) => tx,
                            Err(e) => {
                                eprintln!("Failed to create DAO generic proposal: {e}");
                                exit(2);
                            }
                        };

                        println!("{}", base64::encode(&serialize_async(&tx).await));
                        return drk.stop_rpc_client().await
                    }

                    eprintln!("Unsuported DAO proposal");
                    exit(2);
                }

                println!("{proposal}");

                let mut contract_calls = "\nInvoked contracts:\n".to_string();
                for call in proposal.proposal.auth_calls {
                    contract_calls.push_str(&format!(
                        "\tContract: {}\n\tFunction: {}\n\tData: ",
                        call.contract_id, call.function_code
                    ));

                    if call.auth_data.is_empty() {
                        contract_calls.push_str("-\n");
                        continue;
                    }

                    if call.function_code == DaoFunction::AuthMoneyTransfer as u8 {
                        // We know that the plaintext data live in the data plaintext vec
                        if proposal.data.is_none() {
                            contract_calls.push_str("-\n");
                            continue;
                        }
                        let coin: CoinAttributes =
                            deserialize_async(proposal.data.as_ref().unwrap()).await?;
                        let recipient: Address =
                            StandardAddress::from_public(drk.network, coin.public_key).into();
                        let spend_hook = if coin.spend_hook == FuncId::none() {
                            "-".to_string()
                        } else {
                            format!("{}", coin.spend_hook)
                        };

                        let user_data = if coin.user_data == pallas::Base::ZERO {
                            "-".to_string()
                        } else {
                            format!("{:?}", coin.user_data)
                        };

                        contract_calls.push_str(&format!("\n\t\t{}: {}\n\t\t{}: {} ({})\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\t\t{}: {}\n\n",
                        "Recipient",
                        recipient,
                        "Amount",
                        coin.value,
                        encode_base10(coin.value, BALANCE_BASE10_DECIMALS),
                        "Token",
                        coin.token_id,
                        "Spend hook",
                        spend_hook,
                        "User data",
                        user_data,
                        "Blind",
                        coin.blind));
                    }
                }

                println!("{contract_calls}");

                let votes = drk.get_dao_proposal_votes(&bulla).await?;
                let mut total_yes_vote_value = 0;
                let mut total_no_vote_value = 0;
                let mut total_all_vote_value = 0;
                let mut table = Table::new();
                table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
                table.set_titles(row!["Transaction", "Tokens", "Vote"]);
                for vote in votes {
                    let vote_option = if vote.vote_option {
                        total_yes_vote_value += vote.all_vote_value;
                        "Yes"
                    } else {
                        total_no_vote_value += vote.all_vote_value;
                        "No"
                    };
                    total_all_vote_value += vote.all_vote_value;

                    table.add_row(row![
                        vote.tx_hash,
                        encode_base10(vote.all_vote_value, BALANCE_BASE10_DECIMALS),
                        vote_option
                    ]);
                }

                let outcome = if table.is_empty() {
                    println!("Votes: No votes found");
                    "Unknown"
                } else {
                    println!("Votes:");
                    println!("{table}");
                    println!(
                        "Total tokens votes: {}",
                        encode_base10(total_all_vote_value, BALANCE_BASE10_DECIMALS)
                    );
                    let approval_ratio =
                        (total_yes_vote_value as f64 * 100.0) / total_all_vote_value as f64;
                    println!(
                        "Total tokens Yes votes: {} ({approval_ratio:.2}%)",
                        encode_base10(total_yes_vote_value, BALANCE_BASE10_DECIMALS)
                    );
                    println!(
                        "Total tokens No votes: {} ({:.2}%)",
                        encode_base10(total_no_vote_value, BALANCE_BASE10_DECIMALS),
                        (total_no_vote_value as f64 * 100.0) / total_all_vote_value as f64
                    );

                    let dao = drk.get_dao_by_bulla(&proposal.proposal.dao_bulla).await?;
                    if total_all_vote_value >= dao.params.dao.quorum &&
                        approval_ratio >=
                            (dao.params.dao.approval_ratio_quot /
                                dao.params.dao.approval_ratio_base)
                                as f64
                    {
                        "Approved"
                    } else {
                        "Rejected"
                    }
                };

                if let Some(exec_tx_hash) = proposal.exec_tx_hash {
                    println!("Proposal was executed on transaction: {exec_tx_hash}");
                    return drk.stop_rpc_client().await
                }

                // Retrieve next block height and current block time target,
                // to compute their window.
                let next_block_height = drk.get_next_block_height().await?;
                let block_target = drk.get_block_target().await?;
                let current_window = blockwindow(next_block_height, block_target);
                let end_time = proposal.proposal.creation_blockwindow +
                    proposal.proposal.duration_blockwindows;
                let (voting_status, proposal_status_message) = if current_window < end_time {
                    ("Ongoing", format!("Current proposal outcome: {outcome}"))
                } else {
                    ("Concluded", format!("Proposal outcome: {outcome}"))
                };
                println!("Voting status: {voting_status}");
                println!("{proposal_status_message}");

                drk.stop_rpc_client().await
            }

            DaoSubcmd::ProposalImport => {
                let mut buf = String::new();
                stdin().read_to_string(&mut buf)?;
                let Some(bytes) = base64::decode(buf.trim()) else {
                    eprintln!("Failed to decode encrypted proposal data");
                    exit(2);
                };
                let encrypted_proposal: AeadEncryptedNote = deserialize_async(&bytes).await?;

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;

                // Retrieve all DAOs to try to decrypt the proposal
                let daos = drk.get_daos().await?;
                for dao in &daos {
                    // Check if we have the proposals key
                    let Some(proposals_secret_key) = dao.params.proposals_secret_key else {
                        continue
                    };

                    // Try to decrypt the proposal
                    let Ok(proposal) =
                        encrypted_proposal.decrypt::<ProposalRecord>(&proposals_secret_key)
                    else {
                        continue
                    };

                    let proposal = match drk.get_dao_proposal_by_bulla(&proposal.bulla()).await {
                        Ok(p) => {
                            let mut our_proposal = p;
                            our_proposal.data = proposal.data;
                            our_proposal
                        }
                        Err(_) => proposal,
                    };

                    return drk.put_dao_proposal(&proposal).await
                }

                eprintln!("Couldn't decrypt the proposal with out DAO keys");
                exit(2);
            }

            DaoSubcmd::Vote { bulla, vote, vote_weight } => {
                let bulla = match DaoProposalBulla::from_str(&bulla) {
                    Ok(b) => b,
                    Err(e) => {
                        eprintln!("Invalid proposal bulla: {e}");
                        exit(2);
                    }
                };

                if vote > 1 {
                    eprintln!("Vote can be either 0 (NO) or 1 (YES)");
                    exit(2);
                }
                let vote = vote != 0;

                let weight = match vote_weight {
                    Some(w) => {
                        if let Err(e) = f64::from_str(&w) {
                            eprintln!("Invalid vote weight: {e}");
                            exit(2);
                        }
                        Some(decode_base10(&w, BALANCE_BASE10_DECIMALS, true)?)
                    }
                    None => None,
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;
                let tx = match drk.dao_vote(&bulla, vote, weight).await {
                    Ok(tx) => tx,
                    Err(e) => {
                        eprintln!("Failed to create DAO Vote transaction: {e}");
                        exit(2);
                    }
                };

                println!("{}", base64::encode(&serialize_async(&tx).await));
                drk.stop_rpc_client().await
            }

            DaoSubcmd::Exec { bulla, early } => {
                let bulla = match DaoProposalBulla::from_str(&bulla) {
                    Ok(b) => b,
                    Err(e) => {
                        eprintln!("Invalid proposal bulla: {e}");
                        exit(2);
                    }
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;
                let proposal = drk.get_dao_proposal_by_bulla(&bulla).await?;

                // Identify proposal type by its auth calls
                for call in &proposal.proposal.auth_calls {
                    // We only support transfer right now
                    if call.function_code == DaoFunction::AuthMoneyTransfer as u8 {
                        let tx = match drk.dao_exec_transfer(&proposal, early).await {
                            Ok(tx) => tx,
                            Err(e) => {
                                eprintln!("Failed to execute DAO transfer proposal: {e}");
                                exit(2);
                            }
                        };

                        println!("{}", base64::encode(&serialize_async(&tx).await));
                        return drk.stop_rpc_client().await
                    }
                }

                // If proposal has no auth calls, we consider it a generic one
                if proposal.proposal.auth_calls.is_empty() {
                    let tx = match drk.dao_exec_generic(&proposal, early).await {
                        Ok(tx) => tx,
                        Err(e) => {
                            eprintln!("Failed to execute DAO generic proposal: {e}");
                            exit(2);
                        }
                    };

                    println!("{}", base64::encode(&serialize_async(&tx).await));
                    return drk.stop_rpc_client().await
                }

                eprintln!("Unsuported DAO proposal");
                exit(2);
            }

            DaoSubcmd::SpendHook => {
                let spend_hook =
                    FuncRef { contract_id: *DAO_CONTRACT_ID, func_code: DaoFunction::Exec as u8 }
                        .to_func_id();

                println!("{spend_hook}");

                Ok(())
            }

            DaoSubcmd::MiningConfig { name } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let mut output = vec![];
                if let Err(e) = drk.dao_mining_config(&name, &mut output).await {
                    print_output(&output);
                    eprintln!("Failed to generate DAO mining configuration: {e}");
                    exit(2);
                }
                print_output(&output);

                Ok(())
            }
        },

        Subcmd::AttachFee => {
            let mut tx = parse_tx_from_stdin().await?;

            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                Some(blockchain_config.endpoint),
                &ex,
                args.fun,
            )
            .await;
            if let Err(e) = drk.attach_fee(&mut tx).await {
                eprintln!("Failed to attach the fee call to the transaction: {e}");
                exit(2);
            };

            println!("{}", base64::encode(&serialize_async(&tx).await));

            drk.stop_rpc_client().await
        }

        Subcmd::TxFromCalls { calls_map } => {
            // Parse calls
            let calls = parse_calls_from_stdin().await?;
            if calls.is_empty() {
                eprintln!("No calls were parsed");
                exit(1);
            }

            // If there is a given map, parse it, otherwise construct a
            // linear map.
            let calls_map = match calls_map {
                Some(cmap) => match parse_tree(&cmap) {
                    Ok(v) => v,
                    Err(e) => {
                        eprintln!("Failed parsing calls map: {e}");
                        exit(1);
                    }
                },
                None => {
                    let mut calls_map = Vec::with_capacity(calls.len());
                    for (i, _) in calls.iter().enumerate() {
                        calls_map.push((i, vec![]));
                    }
                    calls_map
                }
            };

            if calls_map.len() != calls.len() {
                eprintln!("Calls map size not equal to parsed calls");
                exit(1);
            }

            // Create a transaction from the mapped calls
            let (mut tx_builder, signature_secrets) = tx_from_calls_mapped(&calls, &calls_map)?;

            // Now build the fee-less tx
            let mut tx = tx_builder.build()?;
            for secrets in &signature_secrets {
                tx.signatures.push(tx.create_sigs(secrets)?);
            }

            // Attach its fee and grab its signature
            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                Some(blockchain_config.endpoint),
                &ex,
                args.fun,
            )
            .await;
            if let Err(e) = drk.attach_fee(&mut tx).await {
                eprintln!("Failed to attach the fee call to the transaction: {e}");
                exit(2);
            };
            // Its safe to unwrap here since we know the fee signature
            // is in the last position.
            let fee_signature = tx.signatures.last().unwrap().clone();

            // Re-sign the tx using the calls secrets
            tx.signatures = vec![];
            for secrets in &signature_secrets {
                tx.signatures.push(tx.create_sigs(secrets)?);
            }
            tx.signatures.push(fee_signature);

            println!("{}", base64::encode(&serialize_async(&tx).await));

            drk.stop_rpc_client().await
        }

        Subcmd::Inspect => {
            let tx = parse_tx_from_stdin().await?;

            println!("{}", pretty_tx(&tx));

            Ok(())
        }

        Subcmd::Broadcast => {
            let tx = parse_tx_from_stdin().await?;

            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                Some(blockchain_config.endpoint),
                &ex,
                args.fun,
            )
            .await;

            if let Err(e) = drk.simulate_tx(&tx).await {
                eprintln!("Failed to simulate tx: {e}");
                exit(2);
            };

            let mut output = vec![];
            if let Err(e) = drk.mark_tx_spend(&tx, &mut output).await {
                print_output(&output);
                eprintln!("Failed to mark transaction coins as spent: {e}");
                exit(2);
            };

            let txid = match drk.broadcast_tx(&tx, &mut output).await {
                Ok(t) => t,
                Err(e) => {
                    print_output(&output);
                    eprintln!("Failed to broadcast transaction: {e}");
                    exit(2);
                }
            };
            print_output(&output);

            println!("Transaction ID: {txid}");

            drk.stop_rpc_client().await
        }

        Subcmd::Scan { reset } => {
            let drk = new_wallet(
                network,
                blockchain_config.cache_path,
                blockchain_config.wallet_path,
                blockchain_config.wallet_pass,
                Some(blockchain_config.endpoint),
                &ex,
                args.fun,
            )
            .await;

            if let Some(height) = reset {
                let mut buf = vec![];
                if let Err(e) = drk.reset_to_height(height, &mut buf).await {
                    print_output(&buf);
                    eprintln!("Failed during wallet reset: {e}");
                    exit(2);
                }
                print_output(&buf);
            }

            if let Err(e) = drk.scan_blocks(&mut vec![], None, &true).await {
                eprintln!("Failed during scanning: {e}");
                exit(2);
            }
            println!("Finished scanning blockchain");

            drk.stop_rpc_client().await
        }

        Subcmd::Explorer { command } => match command {
            ExplorerSubcmd::FetchTx { tx_hash, encode } => {
                let tx_hash = TransactionHash(*blake3::Hash::from_hex(&tx_hash)?.as_bytes());

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;

                let tx = match drk.get_tx(&tx_hash).await {
                    Ok(tx) => tx,
                    Err(e) => {
                        eprintln!("Failed to fetch transaction: {e}");
                        exit(2);
                    }
                };

                let Some(tx) = tx else {
                    println!("Transaction was not found");
                    exit(1);
                };

                // Make sure the tx is correct
                assert_eq!(tx.hash(), tx_hash);

                if encode {
                    println!("{}", base64::encode(&serialize_async(&tx).await));
                    exit(1)
                }

                println!("Transaction ID: {tx_hash}");
                println!("{tx:?}");

                drk.stop_rpc_client().await
            }

            ExplorerSubcmd::SimulateTx => {
                let tx = parse_tx_from_stdin().await?;

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;

                let is_valid = match drk.simulate_tx(&tx).await {
                    Ok(b) => b,
                    Err(e) => {
                        eprintln!("Failed to simulate tx: {e}");
                        exit(2);
                    }
                };

                println!("Transaction ID: {}", tx.hash());
                println!("State: {}", if is_valid { "valid" } else { "invalid" });

                drk.stop_rpc_client().await
            }

            ExplorerSubcmd::TxsHistory { tx_hash, encode } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;

                if let Some(c) = tx_hash {
                    let (tx_hash, status, block_height, tx) = drk.get_tx_history_record(&c).await?;

                    if encode {
                        println!("{}", base64::encode(&serialize_async(&tx).await));
                        exit(1)
                    }

                    println!("Transaction ID: {tx_hash}");
                    println!("Status: {status}");
                    match block_height {
                        Some(block_height) => println!("Block height: {block_height}"),
                        None => println!("Block height: -"),
                    }
                    println!("{tx:?}");

                    return Ok(())
                }

                let map = match drk.get_txs_history() {
                    Ok(m) => m,
                    Err(e) => {
                        eprintln!("Failed to retrieve transactions history records: {e}");
                        exit(2);
                    }
                };

                // Create a prettytable with the new data:
                let mut table = Table::new();
                table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR);
                table.set_titles(row!["Transaction Hash", "Status", "Block Height"]);
                for (txs_hash, status, block_height) in map.iter() {
                    let block_height = match block_height {
                        Some(block_height) => block_height.to_string(),
                        None => String::from("-"),
                    };
                    table.add_row(row![txs_hash, status, block_height]);
                }

                if table.is_empty() {
                    println!("No transactions found");
                } else {
                    println!("{table}");
                }

                Ok(())
            }

            ExplorerSubcmd::ClearReverted => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;

                let mut output = vec![];
                if let Err(e) = drk.remove_reverted_txs(&mut output) {
                    print_output(&output);
                    eprintln!("Failed to remove reverted transactions: {e}");
                    exit(2);
                };
                print_output(&output);

                Ok(())
            }

            ExplorerSubcmd::ScannedBlocks { height } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;

                if let Some(height) = height {
                    let (hash, signing_key) = match drk.get_scanned_block(&height) {
                        Ok(p) => p,
                        Err(e) => {
                            eprintln!("Failed to retrieve scanned block record: {e}");
                            exit(2);
                        }
                    };

                    println!("Height: {height}");
                    println!("Hash: {hash}");
                    println!("Signing key: {signing_key}");

                    return Ok(())
                }

                let map = match drk.get_scanned_block_records() {
                    Ok(m) => m,
                    Err(e) => {
                        eprintln!("Failed to retrieve scanned blocks records: {e}");
                        exit(2);
                    }
                };

                let table = prettytable_scanned_blocks(&map);

                if table.is_empty() {
                    println!("No scanned blocks records found");
                } else {
                    println!("{table}");
                }

                Ok(())
            }

            ExplorerSubcmd::MiningConfig => {
                let (config, recipient, spend_hook, user_data) =
                    parse_mining_config_from_stdin().await?;
                let mut output = vec![];
                display_mining_config(&config, &recipient, &spend_hook, &user_data, &mut output);
                print_output(&output);

                Ok(())
            }
        },

        Subcmd::Alias { command } => match command {
            AliasSubcmd::Add { alias, token } => {
                if alias.chars().count() > 5 {
                    eprintln!("Error: Alias exceeds 5 characters");
                    exit(2);
                }

                let token_id = match TokenId::from_str(token.as_str()) {
                    Ok(t) => t,
                    Err(e) => {
                        eprintln!("Invalid Token ID: {e}");
                        exit(2);
                    }
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let mut output = vec![];
                if let Err(e) = drk.add_alias(alias, token_id, &mut output).await {
                    print_output(&output);
                    eprintln!("Failed to add alias: {e}");
                    exit(2);
                }
                print_output(&output);

                Ok(())
            }

            AliasSubcmd::Show { alias, token } => {
                let token_id = match token {
                    Some(t) => match TokenId::from_str(t.as_str()) {
                        Ok(t) => Some(t),
                        Err(e) => {
                            eprintln!("Invalid Token ID: {e}");
                            exit(2);
                        }
                    },
                    None => None,
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let map = drk.get_aliases(alias, token_id).await?;

                let table = prettytable_aliases(&map);

                if table.is_empty() {
                    println!("No aliases found");
                } else {
                    println!("{table}");
                }

                Ok(())
            }

            AliasSubcmd::Remove { alias } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let mut output = vec![];
                if let Err(e) = drk.remove_alias(alias, &mut output).await {
                    print_output(&output);
                    eprintln!("Failed to remove alias: {e}");
                    exit(2);
                }
                print_output(&output);

                Ok(())
            }
        },

        Subcmd::Token { command } => match command {
            TokenSubcmd::Import { secret_key, token_blind } => {
                let mint_authority = match SecretKey::from_str(&secret_key) {
                    Ok(ma) => ma,
                    Err(e) => {
                        eprintln!("Invalid mint authority: {e}");
                        exit(2);
                    }
                };

                let token_blind = match BaseBlind::from_str(&token_blind) {
                    Ok(tb) => tb,
                    Err(e) => {
                        eprintln!("Invalid token blind: {e}");
                        exit(2);
                    }
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let token_id = drk.import_mint_authority(mint_authority, token_blind).await?;
                println!("Successfully imported mint authority for token ID: {token_id}");

                Ok(())
            }

            TokenSubcmd::GenerateMint => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let mint_authority = SecretKey::random(&mut OsRng);
                let token_blind = BaseBlind::random(&mut OsRng);
                let token_id = drk.import_mint_authority(mint_authority, token_blind).await?;
                println!("Successfully imported mint authority for token ID: {token_id}");

                Ok(())
            }

            TokenSubcmd::List => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;
                let tokens = drk.get_mint_authorities().await?;
                let aliases_map = match drk.get_aliases_mapped_by_token().await {
                    Ok(map) => map,
                    Err(e) => {
                        eprintln!("Failed to fetch wallet aliases: {e}");
                        exit(2);
                    }
                };

                let table = prettytable_tokenlist(&tokens, &aliases_map);

                if table.is_empty() {
                    println!("No tokens found");
                } else {
                    println!("{table}");
                }

                Ok(())
            }

            TokenSubcmd::Mint { token, amount, recipient, spend_hook, user_data } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;

                if let Err(e) = f64::from_str(&amount) {
                    eprintln!("Invalid amount: {e}");
                    exit(2);
                }

                let rcpt = match Address::from_str(&recipient) {
                    Ok(r) => r,
                    Err(e) => {
                        eprintln!("Invalid recipient: {e}");
                        exit(2);
                    }
                };

                if rcpt.network() != drk.network {
                    eprintln!("Recipient address prefix mismatch");
                    exit(2);
                }

                let token_id = match drk.get_token(token).await {
                    Ok(t) => t,
                    Err(e) => {
                        eprintln!("Invalid Token ID: {e}");
                        exit(2);
                    }
                };

                let spend_hook = match spend_hook {
                    Some(s) => match FuncId::from_str(&s) {
                        Ok(s) => Some(s),
                        Err(e) => {
                            eprintln!("Invalid spend hook: {e}");
                            exit(2);
                        }
                    },
                    None => None,
                };

                let user_data = match user_data {
                    Some(u) => {
                        let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
                            Ok(b) => b,
                            Err(e) => {
                                eprintln!("Invalid user data: {e:?}");
                                exit(2);
                            }
                        };

                        match pallas::Base::from_repr(bytes).into() {
                            Some(v) => Some(v),
                            None => {
                                eprintln!("Invalid user data");
                                exit(2);
                            }
                        }
                    }
                    None => None,
                };

                let tx = match drk
                    .mint_token(&amount, *rcpt.public_key(), token_id, spend_hook, user_data)
                    .await
                {
                    Ok(tx) => tx,
                    Err(e) => {
                        eprintln!("Failed to create token mint transaction: {e}");
                        exit(2);
                    }
                };

                println!("{}", base64::encode(&serialize_async(&tx).await));

                drk.stop_rpc_client().await
            }

            TokenSubcmd::Freeze { token } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;
                let token_id = match drk.get_token(token).await {
                    Ok(t) => t,
                    Err(e) => {
                        eprintln!("Invalid Token ID: {e}");
                        exit(2);
                    }
                };

                let tx = match drk.freeze_token(token_id).await {
                    Ok(tx) => tx,
                    Err(e) => {
                        eprintln!("Failed to create token freeze transaction: {e}");
                        exit(2);
                    }
                };

                println!("{}", base64::encode(&serialize_async(&tx).await));

                drk.stop_rpc_client().await
            }
        },

        Subcmd::Contract { command } => match command {
            ContractSubcmd::GenerateDeploy => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;

                let mut output = vec![];
                if let Err(e) = drk.deploy_auth_keygen(&mut output).await {
                    print_output(&output);
                    eprintln!("Error creating deploy auth keypair: {e}");
                    exit(2);
                }
                print_output(&output);

                Ok(())
            }

            ContractSubcmd::List { contract_id } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;

                if let Some(contract_id) = contract_id {
                    let contract_id = match ContractId::from_str(&contract_id) {
                        Ok(d) => d,
                        Err(e) => {
                            eprintln!("Invalid contract id: {e}");
                            exit(2);
                        }
                    };

                    let history = drk.get_deploy_auth_history(&contract_id).await?;

                    let table = prettytable_contract_history(&history);

                    if table.is_empty() {
                        println!("No history records found");
                    } else {
                        println!("{table}");
                    }

                    return Ok(())
                }

                let auths = drk.list_deploy_auth().await?;

                let table = prettytable_contract_auth(&auths);

                if table.is_empty() {
                    println!("No deploy authorities found");
                } else {
                    println!("{table}");
                }

                Ok(())
            }

            ContractSubcmd::ExportData { tx_hash } => {
                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    None,
                    &ex,
                    args.fun,
                )
                .await;

                let pair = drk.get_deploy_history_record_data(&tx_hash).await?;

                println!("{}", base64::encode(&serialize_async(&pair).await));

                Ok(())
            }

            ContractSubcmd::Deploy { deploy_auth, wasm_path, deploy_ix } => {
                // Parse the deployment authority contract id
                let deploy_auth = match ContractId::from_str(&deploy_auth) {
                    Ok(d) => d,
                    Err(e) => {
                        eprintln!("Invalid deploy authority: {e}");
                        exit(2);
                    }
                };

                // Read the wasm bincode and deploy instruction
                let wasm_bin = smol::fs::read(expand_path(&wasm_path)?).await?;
                let deploy_ix = match deploy_ix {
                    Some(p) => smol::fs::read(expand_path(&p)?).await?,
                    None => vec![],
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;

                let tx = match drk.deploy_contract(&deploy_auth, wasm_bin, deploy_ix).await {
                    Ok(tx) => tx,
                    Err(e) => {
                        eprintln!("Error creating contract deployment tx: {e}");
                        exit(2);
                    }
                };

                println!("{}", base64::encode(&serialize_async(&tx).await));

                drk.stop_rpc_client().await
            }

            ContractSubcmd::Lock { deploy_auth } => {
                // Parse the deployment authority contract id
                let deploy_auth = match ContractId::from_str(&deploy_auth) {
                    Ok(d) => d,
                    Err(e) => {
                        eprintln!("Invalid deploy authority: {e}");
                        exit(2);
                    }
                };

                let drk = new_wallet(
                    network,
                    blockchain_config.cache_path,
                    blockchain_config.wallet_path,
                    blockchain_config.wallet_pass,
                    Some(blockchain_config.endpoint),
                    &ex,
                    args.fun,
                )
                .await;

                let tx = match drk.lock_contract(&deploy_auth).await {
                    Ok(tx) => tx,
                    Err(e) => {
                        eprintln!("Error creating contract lock tx: {e}");
                        exit(2);
                    }
                };

                println!("{}", base64::encode(&serialize_async(&tx).await));

                drk.stop_rpc_client().await
            }
        },
    }
}
