1#![allow(non_snake_case)]
8
9mod clock;
10mod control;
11mod db;
12mod message;
13mod network;
14mod snapshot;
15mod state;
16mod utils;
17
18#[derive(clap::Parser, Debug)]
20#[command(author, version, about, long_about = None)]
21struct Args {
22 #[arg(long, default_value_t = String::new())]
24 cli_site_id: String,
25
26 #[arg(long, default_value_t = 0)]
28 cli_port: u16,
29
30 #[arg(long, value_delimiter = ',')]
32 cli_peers: Vec<String>,
33
34 #[arg(long, default_value_t = String::from("127.0.0.1"))]
36 cli_ip: String,
37
38 #[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 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 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 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 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 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 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 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 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#[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#[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}