1#![deny(clippy::pedantic, clippy::nursery)]
2
3mod config_commands;
4mod ecash_commands;
5mod general_commands;
6mod lightning_commands;
7mod onchain_commands;
8
9use std::collections::BTreeMap;
10
11use bitcoin::Txid;
12use bitcoin::address::NetworkUnchecked;
13use clap::{CommandFactory, Parser, Subcommand};
14use config_commands::ConfigCommands;
15use ecash_commands::EcashCommands;
16use fedimint_connectors::ConnectorRegistry;
17use fedimint_connectors::error::ServerError;
18use fedimint_core::PeerId;
19use fedimint_core::config::FederationId;
20use fedimint_core::invite_code::InviteCode;
21use fedimint_core::util::SafeUrl;
22use fedimint_gateway_common::{
23 ChannelInfo, CloseChannelsWithPeerResponse, CreateOfferResponse, FederationConfig,
24 FederationInfo, GatewayBalances, GatewayFedConfig, GatewayInfo, GetInvoiceResponse,
25 ListTransactionsResponse, MnemonicResponse, PayOfferResponse, PaymentLogResponse,
26 PaymentSummaryResponse, ReceiveEcashResponse, SpendEcashResponse, WithdrawResponse,
27};
28use fedimint_ln_common::client::GatewayApi;
29use fedimint_logging::TracingSetup;
30use general_commands::GeneralCommands;
31use lightning_commands::LightningCommands;
32use onchain_commands::OnchainCommands;
33use serde::Serialize;
34
35#[derive(Serialize)]
41#[serde(untagged)]
42pub enum CliOutput {
43 Info(GatewayInfo),
45 Balances(GatewayBalances),
46 Federation(FederationInfo),
47 Mnemonic(MnemonicResponse),
48 PaymentLog(PaymentLogResponse),
49 PaymentSummary(PaymentSummaryResponse),
50 InviteCodes(BTreeMap<FederationId, BTreeMap<PeerId, (String, InviteCode)>>),
51 PasswordHash(String),
52
53 Invoice {
55 invoice: String,
56 },
57 Preimage {
58 preimage: String,
59 },
60 FundingTxid {
61 funding_txid: Txid,
62 },
63 Channels(Vec<ChannelInfo>),
64 CloseChannels(CloseChannelsWithPeerResponse),
65 InvoiceDetails(Option<GetInvoiceResponse>),
66 Transactions(ListTransactionsResponse),
67 Offer(CreateOfferResponse),
68 OfferPayment(PayOfferResponse),
69
70 DepositAddress {
72 address: bitcoin::Address<NetworkUnchecked>,
73 },
74 DepositRecheck(serde_json::Value),
75 PeginTxid {
76 txid: Txid,
77 },
78 Withdraw(WithdrawResponse),
79 SpendEcash(SpendEcashResponse),
80 ReceiveEcash(ReceiveEcashResponse),
81
82 OnchainAddress {
84 address: String,
85 },
86 SendOnchainTxid {
87 txid: Txid,
88 },
89
90 Config(GatewayFedConfig),
92 FederationConfigs(Vec<FederationConfig>),
93
94 #[serde(skip)]
96 Empty,
97}
98
99pub type CliOutputResult = Result<CliOutput, ServerError>;
102
103#[derive(Debug, Clone, Copy, Serialize)]
108#[serde(rename_all = "snake_case")]
109pub enum ErrorCode {
110 AuthFailed,
112 ConnectionFailed,
114 InvalidInput,
116 NotFound,
118 InvalidState,
120 Timeout,
122 Internal,
124 Unknown,
126}
127
128#[derive(Debug, Clone, Copy)]
133#[repr(i32)]
134pub enum ExitCode {
135 Success = 0,
136 GeneralError = 1,
137 ConnectionError = 2,
138 AuthError = 3,
139 InvalidInput = 4,
140 NotFound = 5,
141 Timeout = 6,
142}
143
144impl From<ErrorCode> for ExitCode {
145 fn from(code: ErrorCode) -> Self {
146 match code {
147 ErrorCode::AuthFailed => Self::AuthError,
148 ErrorCode::ConnectionFailed => Self::ConnectionError,
149 ErrorCode::InvalidInput => Self::InvalidInput,
150 ErrorCode::NotFound => Self::NotFound,
151 ErrorCode::Timeout => Self::Timeout,
152 ErrorCode::InvalidState | ErrorCode::Internal | ErrorCode::Unknown => {
153 Self::GeneralError
154 }
155 }
156 }
157}
158
159#[derive(Debug, Serialize)]
164pub struct CliError {
165 pub error: String,
167
168 pub code: ErrorCode,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
173 pub cause: Option<String>,
174}
175
176impl CliError {
177 fn from_server_error(err: &ServerError) -> Self {
182 let error = err.to_string();
183 let code = Self::classify_server_error(err);
184 let cause = std::error::Error::source(err).map(ToString::to_string);
185
186 Self { error, code, cause }
187 }
188
189 const fn classify_server_error(err: &ServerError) -> ErrorCode {
191 match err {
192 ServerError::Connection(_) | ServerError::Transport(_) => ErrorCode::ConnectionFailed,
194
195 ServerError::InvalidPeerId { .. }
197 | ServerError::InvalidPeerUrl { .. }
198 | ServerError::InvalidEndpoint(_)
199 | ServerError::InvalidRpcId(_) => ErrorCode::InvalidInput,
200
201 ServerError::InvalidRequest(_)
203 | ServerError::ResponseDeserialization(_)
204 | ServerError::InvalidResponse(_)
205 | ServerError::ServerError(_)
206 | ServerError::InternalClientError(_) => ErrorCode::Internal,
207
208 ServerError::ConditionFailed(_) => ErrorCode::NotFound,
210
211 _ => ErrorCode::Unknown,
213 }
214 }
215}
216
217#[derive(Parser)]
218#[command(version)]
219struct Cli {
220 #[clap(long, short, default_value = "http://127.0.0.1:80")]
222 address: SafeUrl,
223
224 #[command(subcommand)]
226 command: Commands,
227
228 #[clap(long)]
230 rpcpassword: Option<String>,
231}
232
233#[derive(Subcommand)]
234enum Commands {
235 #[command(flatten)]
236 General(GeneralCommands),
237 #[command(subcommand)]
238 Lightning(LightningCommands),
239 #[command(subcommand)]
240 Ecash(EcashCommands),
241 #[command(subcommand)]
242 Onchain(OnchainCommands),
243 #[command(subcommand)]
244 Cfg(ConfigCommands),
245 Completion {
246 shell: clap_complete::Shell,
247 },
248}
249
250#[tokio::main]
251async fn main() {
252 if let Err(err) = TracingSetup::default().init() {
253 let cli_err = CliError {
254 error: format!("Failed to initialize logging: {err}"),
255 code: ErrorCode::Internal,
256 cause: None,
257 };
258 print_response(&cli_err);
259 std::process::exit(ExitCode::GeneralError as i32);
260 }
261
262 if let Err(err) = run().await {
263 let cli_err = CliError::from_server_error(&err);
264 let exit_code = ExitCode::from(cli_err.code);
265 print_response(&cli_err);
266 std::process::exit(exit_code as i32);
267 }
268}
269
270async fn run() -> CliOutputResult {
271 let cli = Cli::parse();
272 let connector_registry = ConnectorRegistry::build_from_client_defaults()
273 .with_env_var_overrides()
274 .map_err(ServerError::InternalClientError)?
275 .bind()
276 .await
277 .map_err(ServerError::InternalClientError)?;
278 let client = GatewayApi::new(cli.rpcpassword, connector_registry);
279
280 let output = match cli.command {
281 Commands::General(general_command) => general_command.handle(&client, &cli.address).await?,
282 Commands::Lightning(lightning_command) => {
283 lightning_command.handle(&client, &cli.address).await?
284 }
285 Commands::Ecash(ecash_command) => ecash_command.handle(&client, &cli.address).await?,
286 Commands::Onchain(onchain_command) => onchain_command.handle(&client, &cli.address).await?,
287 Commands::Cfg(config_commands) => config_commands.handle(&client, &cli.address).await?,
288 Commands::Completion { shell } => {
289 clap_complete::generate(
290 shell,
291 &mut Cli::command(),
292 "gateway-cli",
293 &mut std::io::stdout(),
294 );
295 return Ok(CliOutput::Empty);
296 }
297 };
298
299 if !matches!(output, CliOutput::Empty) {
301 print_response(&output);
302 }
303
304 Ok(output)
305}
306
307fn print_response<T: Serialize>(val: T) {
308 println!(
309 "{}",
310 serde_json::to_string_pretty(&val).expect("Cannot serialize")
311 );
312}