1use std::{
2 fs::File,
3 io::Write,
4 net::{IpAddr, SocketAddr},
5 ops::Deref,
6 path::PathBuf,
7 sync::Arc,
8};
9
10use anyhow::Result;
11use axum::{
12 extract::{self, Query, State},
13 response::IntoResponse,
14 routing::{get, post},
15 Json, Router,
16};
17use clap::Args;
18use reqwest::StatusCode;
19use serde_json::json;
20use tracing_appender::non_blocking::NonBlocking;
21
22use crate::{
23 cli::{make_env_filter, ReloadHandler},
24 Block, DbLedger, Network, Transaction,
25};
26
27#[derive(Debug, Args, Clone)]
29pub struct LedgerQuery<N: Network> {
30 #[arg(long, default_value = "3030")]
32 pub port: u16,
33
34 #[arg(long, default_value = "0.0.0.0")]
36 pub bind: IpAddr,
37
38 #[arg(long)]
40 pub readonly: bool,
41
42 #[arg(long)]
45 pub record: bool,
46
47 #[arg(long, short, default_value = "transactions.json")]
49 pub output: PathBuf,
50
51 #[clap(skip)]
52 phantom: std::marker::PhantomData<N>,
53}
54
55struct LedgerState<N: Network> {
56 readonly: bool,
57 ledger: DbLedger<N>,
58 appender: Option<NonBlocking>,
59 log_level_handler: ReloadHandler,
60}
61
62type AppState<N> = Arc<LedgerState<N>>;
63
64impl<N: Network> LedgerQuery<N> {
65 #[tokio::main]
66 pub async fn parse(self, ledger: &DbLedger<N>, log_level_handler: ReloadHandler) -> Result<()> {
67 let (appender, _guard) = if self.record {
68 let (appender, guard) = tracing_appender::non_blocking(
69 File::options()
70 .create(true)
71 .append(true)
72 .open(self.output.clone())
73 .expect("Failed to open the file for writing transactions"),
74 );
75 (Some(appender), Some(guard))
76 } else {
77 (None, None)
78 };
79
80 let state = LedgerState {
81 readonly: self.readonly,
82 ledger: ledger.clone(),
83 appender,
84 log_level_handler,
85 };
86
87 let network = N::str_id();
88
89 let app = Router::new()
90 .route(
91 &format!("/{network}/latest/stateRoot"),
92 get(Self::latest_state_root),
93 )
94 .route(
95 &format!("/{network}/block/height/latest"),
96 get(Self::latest_height),
97 )
98 .route(
99 &format!("/{network}/block/hash/latest"),
100 get(Self::latest_hash),
101 )
102 .route(
103 &format!("/{network}/transaction/broadcast"),
104 post(Self::broadcast_tx),
105 )
106 .route("/block", post(Self::add_block))
107 .route("/log", post(Self::set_log_level))
108 .with_state(Arc::new(state));
111
112 let listener = tokio::net::TcpListener::bind(SocketAddr::new(self.bind, self.port)).await?;
113 tracing::info!("listening on: {:?}", listener.local_addr().unwrap());
114 axum::serve(listener, app).await?;
115
116 Ok(())
117 }
118
119 async fn latest_state_root(state: State<AppState<N>>) -> impl IntoResponse {
120 Json(json!(state.ledger.latest_state_root()))
121 }
122
123 async fn latest_height(state: State<AppState<N>>) -> impl IntoResponse {
124 Json(json!(state.ledger.latest_height()))
125 }
126
127 async fn latest_hash(state: State<AppState<N>>) -> impl IntoResponse {
128 Json(json!(state.ledger.latest_hash()))
129 }
130
131 async fn broadcast_tx(
132 state: State<AppState<N>>,
133 payload: extract::Json<Transaction<N>>,
134 ) -> impl IntoResponse {
135 let Ok(tx_json) = serde_json::to_string(payload.deref()) else {
136 return StatusCode::BAD_REQUEST;
137 };
138
139 if let Some(mut a) = state.appender.clone() {
140 match write!(a, "{}", tx_json) {
141 Ok(_) => StatusCode::OK,
142 Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
143 }
144 } else {
145 println!("{}", tx_json);
146 StatusCode::OK
147 }
148 }
149
150 async fn add_block(
151 state: State<AppState<N>>,
152 payload: extract::Json<Block<N>>,
153 ) -> impl IntoResponse {
154 if state.readonly {
155 return (StatusCode::FORBIDDEN, Json(json!({"error": "readonly"})));
156 }
157
158 if state.ledger.latest_hash() != payload.previous_hash()
159 || state.ledger.latest_state_root() != payload.previous_state_root()
160 || state.ledger.latest_height() + 1 != payload.height()
161 {
162 return (
163 StatusCode::BAD_REQUEST,
164 Json(json!({"error": "invalid block"})),
165 );
166 }
167
168 if let Err(e) = state
169 .ledger
170 .check_next_block(&payload, &mut rand::thread_rng())
171 {
172 return (
173 StatusCode::INTERNAL_SERVER_ERROR,
174 Json(json!({"error": format!("failed to validate block: {e}")})),
175 );
176 }
177
178 match state.ledger.advance_to_next_block(&payload) {
179 Ok(_) => (StatusCode::OK, Json(json!({"status": "ok"}))),
180 Err(e) => (
181 StatusCode::INTERNAL_SERVER_ERROR,
182 Json(json!({"error": format!("failed to advance block: {e}")})),
183 ),
184 }
185 }
186
187 async fn set_log_level(
188 state: State<AppState<N>>,
189 Query(verbosity): Query<u8>,
190 ) -> impl IntoResponse {
191 let Ok(_) = state
192 .log_level_handler
193 .modify(|filter| *filter = make_env_filter(verbosity))
194 else {
195 return (
196 StatusCode::INTERNAL_SERVER_ERROR,
197 Json(json!({"error": "failed to set log level"})),
198 );
199 };
200
201 (StatusCode::OK, Json(json!({"status": "ok"})))
202 }
203}