1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
// Copyright (c) 2022 MASSA LABS <info@massa.net>
//! Massa stateless CLI
#![warn(missing_docs)]
#![warn(unused_crate_dependencies)]
use crate::settings::SETTINGS;
use anyhow::Result;
use clap::{crate_version, Parser};
use cmds::Command;
use console::style;
use dialoguer::Password;
use is_terminal::IsTerminal;
use massa_sdk::{Client, ClientConfig, HttpConfig};
use massa_wallet::Wallet;
use serde::Serialize;
use std::env;
use std::net::IpAddr;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};

mod cmds;
mod display;
mod repl;
mod settings;

#[cfg(test)]
pub mod tests;

#[derive(Parser)]
#[command(version = crate_version!())]
struct Args {
    /// Port to listen on (Massa public API).
    #[arg(long)]
    public_port: Option<u16>,
    /// Port to listen on (Massa private API).
    #[arg(long)]
    private_port: Option<u16>,
    /// Port to listen on (Massa GRPC Public API).
    #[arg(long)]
    grpc_public_port: Option<u16>,
    /// Port to listen on (Massa GRPC Private API).
    #[arg(long)]
    grpc_private_port: Option<u16>,
    /// Chain id
    #[arg(long)]
    chain_id: Option<u64>,
    /// Address to listen on
    #[arg(long)]
    ip: Option<IpAddr>,
    /// Command that client would execute (non-interactive mode)
    #[arg(name = "COMMAND", default_value = "help")]
    command: Command,
    /// Optional command parameter (as a JSON string)
    #[arg(name = "PARAMETERS")]
    parameters: Vec<String>,
    /// Path of wallet folder
    #[arg(short = 'w', long = "wallet", default_value = "wallets/")]
    wallet: PathBuf,
    /// Enable a mode where input/output are serialized as JSON
    #[arg(short = 'j', long = "json")]
    json: bool,
    #[arg(short = 'p', long = "pwd")]
    /// Wallet password
    password: Option<String>,
}

#[derive(Serialize)]
struct JsonError {
    error: String,
}

/// Ask for the wallet password
/// If the wallet does not exist, it will require password confirmation
pub(crate) fn ask_password(wallet_path: &Path) -> String {
    if wallet_path.is_dir() {
        Password::new()
            .with_prompt("Enter wallet password")
            .interact()
            .expect("IO error: Password reading failed, walled couldn't be unlocked")
    } else {
        Password::new()
            .with_prompt("Enter new password for wallet")
            .with_confirmation("Confirm password", "Passwords mismatching")
            .interact()
            .expect("IO error: Password reading failed, wallet couldn't be created")
    }
}

fn main() -> anyhow::Result<()> {
    let args = Args::parse();
    let tokio_rt = tokio::runtime::Builder::new_multi_thread()
        .thread_name_fn(|| {
            static ATOMIC_ID: AtomicUsize = AtomicUsize::new(0);
            let id = ATOMIC_ID.fetch_add(1, Ordering::SeqCst);
            format!("tokio-client-{}", id)
        })
        .enable_all()
        .build()
        .unwrap();

    tokio_rt.block_on(run(args))
}

async fn run(args: Args) -> Result<()> {
    let client_config = ClientConfig {
        max_request_body_size: SETTINGS.client.max_request_body_size,
        request_timeout: SETTINGS.client.request_timeout,
        max_concurrent_requests: SETTINGS.client.max_concurrent_requests,
        certificate_store: SETTINGS.client.certificate_store.clone(),
        id_kind: SETTINGS.client.id_kind.clone(),
        max_log_length: SETTINGS.client.max_log_length,
        headers: SETTINGS.client.headers.clone(),
    };

    let http_config = HttpConfig {
        client_config,
        enabled: SETTINGS.client.http.enabled,
    };

    // TODO: move settings loading in another crate ... see #1277
    let settings = SETTINGS.clone();

    let address = match args.ip {
        Some(ip) => ip,
        None => settings.default_node.ip,
    };
    let public_port = match args.public_port {
        Some(public_port) => public_port,
        None => settings.default_node.public_port,
    };
    let private_port = match args.private_port {
        Some(private_port) => private_port,
        None => settings.default_node.private_port,
    };
    let grpc_port = match args.grpc_public_port {
        Some(grpc_port) => grpc_port,
        None => settings.default_node.grpc_public_port,
    };
    let grpc_priv_port = match args.grpc_private_port {
        Some(grpc_port) => grpc_port,
        None => settings.default_node.grpc_private_port,
    };
    let chain_id = match args.chain_id {
        Some(chain_id) => chain_id,
        None => match settings.default_node.chain_id {
            Some(chain_id) => chain_id,
            None => *massa_models::config::constants::CHAINID,
        },
    };

    // Setup panic handlers,
    // and when a panic occurs,
    // run default handler,
    // and then shutdown.
    let default_panic = std::panic::take_hook();
    std::panic::set_hook(Box::new(move |info| {
        default_panic(info);
        std::process::exit(1);
    }));

    // Note: grpc handler requires a mut handler
    let mut client = Client::new(
        address,
        public_port,
        private_port,
        grpc_port,
        grpc_priv_port,
        chain_id,
        &http_config,
    )
    .await?;
    if std::io::stdout().is_terminal() && args.command == Command::help && !args.json {
        // Interactive mode
        repl::run(&mut client, &args.wallet, args.password).await?;
    } else {
        // Non-Interactive mode

        // Only prompt for password if the command needs wallet access.
        let mut wallet_opt = match args.command.is_pwd_needed() {
            true => {
                let password = match (args.password, env::var("MASSA_CLIENT_PASSWORD")) {
                    (Some(pwd), _) => pwd,
                    (_, Ok(pwd)) => pwd,
                    _ => ask_password(&args.wallet),
                };

                let wallet = Wallet::new(args.wallet, password, chain_id)?;
                Some(wallet)
            }
            false => None,
        };

        match args
            .command
            .run(&mut client, &mut wallet_opt, &args.parameters, args.json)
            .await
        {
            Ok(output) => {
                if args.json {
                    output
                        .stdout_json()
                        .expect("fail to serialize to JSON command output")
                } else {
                    output.pretty_print();
                }
            }
            Err(e) => {
                if args.json {
                    let error = serde_json::to_string(&JsonError {
                        error: format!("{:?}", e),
                    })
                    .expect("fail to serialize to JSON error");
                    println!("{}", error);
                } else {
                    println!("{}", style(format!("Error: {}", e)).red());
                }
            }
        }
    }
    Ok(())
}