peillute/
main.rs

1//! Peillute - A distributed financial transaction system
2//!
3//! This module serves as the main entry point for the Peillute application, handling both
4//! server and client-side functionality. The application supports distributed transactions
5//! with vector clock synchronization and peer-to-peer communication.
6
7#![allow(non_snake_case)]
8
9mod clock;
10mod control;
11mod db;
12mod message;
13mod network;
14mod snapshot;
15mod state;
16mod utils;
17
18/// Command-line arguments for configuring the Peillute application
19#[derive(clap::Parser, Debug)]
20#[command(author, version, about, long_about = None)]
21struct Args {
22    /// Unique identifier for this site in the network
23    #[arg(long, default_value_t = String::new())]
24    cli_site_id: String,
25
26    /// Port number for peer-to-peer communication
27    #[arg(long, default_value_t = 0)]
28    cli_port: u16,
29
30    /// List of peer addresses to connect to
31    #[arg(long, value_delimiter = ',')]
32    cli_peers: Vec<String>,
33
34    /// IP address to bind to
35    #[arg(long, default_value_t = String::from("127.0.0.1"))]
36    cli_ip: String,
37
38    /// ID for the batabase path
39    #[arg(long, default_value_t = 0)]
40    cli_db_id: u16,
41}
42
43#[cfg(feature = "server")]
44#[tokio::main]
45async fn main() -> rusqlite::Result<(), Box<dyn std::error::Error>> {
46    use crate::state::LOCAL_APP_STATE;
47    use clap::Parser;
48    use std::io::{self as std_io, Write};
49    use std::net::SocketAddr;
50    use tokio::io::{self as tokio_io, AsyncBufReadExt, BufReader};
51    use tokio::net::TcpListener;
52
53    const LOW_PORT: u16 = 10000;
54    const HIGH_PORT: u16 = 11000;
55    const PORT_OFFSET: u16 = HIGH_PORT - LOW_PORT + 1;
56
57    if !db::is_database_initialized()? {
58        let _ = db::init_db();
59    }
60
61    control::control_worker();
62    // Init the logger
63    env_logger::init();
64
65    let args = Args::parse();
66
67    let port_range = LOW_PORT..=HIGH_PORT;
68    let selected_port = if args.cli_port == 0 {
69        port_range
70            .into_iter()
71            .find(|port| std::net::TcpListener::bind(("127.0.0.1", *port)).is_ok())
72            .unwrap_or(LOW_PORT)
73    } else {
74        args.cli_port
75    };
76
77    let final_site_addr: SocketAddr = format!("{}:{}", &args.cli_ip, selected_port).parse()?;
78    let client_server_interaction_addr: SocketAddr =
79        format!("{}:{}", &args.cli_ip, selected_port + PORT_OFFSET).parse()?;
80
81    let final_cli_peers_addrs: Vec<SocketAddr> = args
82        .cli_peers
83        .into_iter()
84        .filter_map(|peer| peer.parse::<SocketAddr>().ok())
85        .collect();
86
87    let (final_site_id, final_clock, needs_sync) = match utils::reload_existing_site().await {
88        Ok((site_id_from_db, clock_from_db)) => (site_id_from_db, clock_from_db, true),
89        Err(_) => {
90            let generated_site_id = if args.cli_site_id.is_empty() {
91                utils::get_mac_address().unwrap_or_default() + "_" + &std::process::id().to_string()
92            } else {
93                args.cli_site_id.clone()
94            };
95            (generated_site_id, crate::clock::Clock::new(), false)
96        }
97    };
98
99    {
100        let mut state = LOCAL_APP_STATE.lock().await;
101        state.init_site_id(final_site_id.clone());
102        state.init_site_addr(final_site_addr);
103        state.init_clock(final_clock);
104        state.init_parent_addr_for_transaction_wave();
105        state.init_cli_peer_addrs(final_cli_peers_addrs);
106        state.init_sync(needs_sync);
107    }
108
109    // Create the network listener
110    let network_listener_local_addr = final_site_addr.clone();
111    let listener: TcpListener = TcpListener::bind(network_listener_local_addr).await?;
112    log::debug!("Listening on: {}", network_listener_local_addr);
113
114    // Create the web app listener
115    let router = axum::Router::new().serve_dioxus_application(ServeConfigBuilder::default(), App);
116    let router = router.into_make_service();
117    let backend_listener = tokio::net::TcpListener::bind(client_server_interaction_addr)
118        .await
119        .unwrap();
120
121    // Create the stdin listener for the CLI
122    let stdin: tokio_io::Stdin = tokio_io::stdin();
123    let reader: BufReader<tokio_io::Stdin> = BufReader::new(stdin);
124    let mut lines: tokio_io::Lines<_> = reader.lines();
125
126    // Announce our presence to the network
127    network::announce(&args.cli_ip, LOW_PORT, HIGH_PORT, selected_port).await;
128
129    println!(
130        "\n\
131        ===================================================\n\
132            💰  Welcome to Peillute! 💰\n\
133        ===================================================\n\
134        \n\
135            📌 Write /help to get the command list.\n\
136            🌐 Access the web interface at: http://{}\n\
137        ===================================================\n\
138        ",
139        client_server_interaction_addr
140    );
141    print!("> ");
142    std_io::stdout().flush().unwrap();
143
144    let main_loop_app_state = LOCAL_APP_STATE.clone();
145
146    // Spawn the web server
147    let server_task = tokio::spawn(async move {
148        axum::serve(backend_listener, router).await.unwrap();
149    });
150
151    main_loop(main_loop_app_state, &mut lines, listener).await;
152
153    // Ensure the server task finishes cleanly if ever reached
154    server_task.await?;
155
156    Ok(())
157}
158
159#[cfg(feature = "server")]
160async fn main_loop(
161    _state: std::sync::Arc<tokio::sync::Mutex<crate::state::AppState>>,
162    lines: &mut tokio::io::Lines<tokio::io::BufReader<tokio::io::Stdin>>,
163    listener: tokio::net::TcpListener,
164) {
165    use crate::control::{parse_command, process_cli_command};
166    use std::io::{self as std_io, Write};
167    use tokio::select;
168
169    loop {
170        select! {
171            line = lines.next_line() => {
172                let command = parse_command(line);
173                if let Err(e) = process_cli_command(command).await{
174                    log::error!("Error handling a cli command:\n{}", e);
175                }
176                print!("> ");
177                std_io::stdout().flush().unwrap();
178            }
179            Ok((stream, addr)) = listener.accept() => {
180                let _ = crate::network::start_listening(stream, addr).await;
181            }
182            _ = tokio::signal::ctrl_c() => {
183                disconnect().await;
184                std::process::exit(0);
185            }
186        }
187    }
188}
189
190#[cfg(feature = "server")]
191async fn disconnect() {
192    use crate::message::{MessageInfo, NetworkMessageCode};
193    use crate::state::LOCAL_APP_STATE;
194    use log::{error, info};
195
196    let (local_addr, site_id, connected_nei_addr) = {
197        let state = LOCAL_APP_STATE.lock().await;
198        (
199            state.get_site_addr(),
200            state.get_site_id().to_string(),
201            state.get_connected_nei_addr(),
202        )
203    };
204
205    info!("Shutting down site {}.", site_id);
206    for peer_addr in connected_nei_addr {
207        // increment the clock for every deconnection
208        let clock = {
209            let mut state = LOCAL_APP_STATE.lock().await;
210            state.update_clock(None).await;
211            state.get_clock().clone()
212        };
213
214        if let Err(e) = crate::network::send_message(
215            peer_addr,
216            MessageInfo::None,
217            None,
218            NetworkMessageCode::Disconnect,
219            local_addr,
220            &site_id,
221            &site_id,
222            local_addr,
223            clock.clone(),
224        )
225        .await
226        {
227            error!("Error sending message to {}: {}", peer_addr, e);
228        }
229    }
230}
231
232use dioxus::prelude::*;
233
234#[cfg(not(feature = "server"))]
235fn main() {
236    dioxus::launch(App);
237}
238
239mod views;
240use views::*;
241
242const FAVICON: Asset = asset!("/assets/icon.png");
243const MAIN_CSS: Asset = asset!("/assets/styling/main.css");
244
245/// Main application component that sets up the web interface
246#[component]
247fn App() -> Element {
248    rsx! {
249        document::Link { rel: "icon", href: FAVICON }
250        document::Link { rel: "stylesheet", href: MAIN_CSS }
251
252        Router::<Route> {}
253    }
254}
255
256/// Defines the routing structure for the web application
257#[derive(Debug, Clone, Routable, PartialEq)]
258#[rustfmt::skip]
259enum Route {
260    #[layout(Navbar)]
261        #[route("/")]
262        Home {},
263        #[route("/info")]
264        Info {},
265        #[nest("/:name")]
266        #[layout(User)]
267            #[route("/history")]
268            History {
269                name: String,
270            },
271            #[route("/withdraw")]
272            Withdraw {
273                name: String,
274            },
275            #[route("/pay")]
276            Pay {
277                name: String,
278            },
279            #[route("/refund")]
280            Refund {
281                name: String,
282            },
283            #[route("/transfer")]
284            Transfer {
285                name: String,
286            },
287            #[route("/deposit")]
288            Deposit {
289                name: String,
290            },
291}
292
293#[cfg(test)]
294mod tests {
295    use clap::Parser;
296    #[test]
297    fn test_args_parsing() {
298        use super::Args;
299        let args = Args::parse_from(vec![
300            "my_program",
301            "--cli-site-id",
302            "A",
303            "--cli-port",
304            "8080",
305            "--cli-peers",
306            "127.0.0.1:8081,127.0.0.1:8082",
307        ]);
308        assert_eq!(args.cli_site_id, "A");
309        assert_eq!(args.cli_port, 8080);
310        assert_eq!(args.cli_peers.len(), 2);
311        assert_eq!(args.cli_peers[0], "127.0.0.1:8081");
312        assert_eq!(args.cli_peers[1], "127.0.0.1:8082");
313    }
314
315    #[test]
316    fn test_args_parsing_no_peers() {
317        use super::Args;
318        let args = Args::parse_from(vec![
319            "my_program",
320            "--cli-site-id",
321            "A",
322            "--cli-port",
323            "8080",
324        ]);
325        assert_eq!(args.cli_site_id, "A");
326        assert_eq!(args.cli_port, 8080);
327        assert_eq!(args.cli_peers.len(), 0);
328    }
329}