1use axum::{
22 extract::{Path, State},
23 http::StatusCode,
24 response::IntoResponse,
25 routing::{get, post},
26 Json, Router,
27};
28use guts_consensus::{SerializablePublicKey, SerializableSignature, Transaction};
29use serde::{Deserialize, Serialize};
30
31use crate::api::AppState;
32
33#[derive(Serialize)]
35pub struct ConsensusStatusResponse {
36 pub enabled: bool,
38 pub state: String,
40 pub view: u64,
42 pub finalized_height: u64,
44 pub current_leader: Option<String>,
46 pub is_leader: bool,
48 pub pending_transactions: usize,
50}
51
52#[derive(Serialize)]
54pub struct BlockInfoResponse {
55 pub height: u64,
57 pub block_id: String,
59 pub parent_id: String,
61 pub producer: String,
63 pub timestamp: u64,
65 pub tx_count: usize,
67 pub tx_root: String,
69 pub state_root: String,
71 pub view: u64,
73 pub signature_count: usize,
75}
76
77#[derive(Serialize)]
79pub struct ValidatorInfoResponse {
80 pub name: String,
82 pub pubkey: String,
84 pub weight: u64,
86 pub addr: String,
88 pub active: bool,
90}
91
92#[derive(Serialize)]
94pub struct ValidatorSetResponse {
95 pub epoch: u64,
97 pub total_weight: u64,
99 pub quorum_weight: u64,
101 pub validator_count: usize,
103 pub validators: Vec<ValidatorInfoResponse>,
105}
106
107#[derive(Serialize)]
109pub struct MempoolStatsResponse {
110 pub transaction_count: usize,
112 pub oldest_age_secs: f64,
114 pub average_propose_count: f64,
116}
117
118#[derive(Deserialize)]
120#[serde(tag = "type")]
121pub enum SubmitTransactionRequest {
122 CreateRepository {
124 owner: String,
125 name: String,
126 description: String,
127 default_branch: String,
128 visibility: String,
129 creator_pubkey: String,
130 signature: String,
131 },
132 CreateIssue {
134 repo_key: String,
135 title: String,
136 body: String,
137 author: String,
138 creator_pubkey: String,
139 signature: String,
140 },
141 CreatePullRequest {
143 repo_key: String,
144 title: String,
145 description: String,
146 author: String,
147 source_branch: String,
148 target_branch: String,
149 source_commit: String,
150 target_commit: String,
151 creator_pubkey: String,
152 signature: String,
153 },
154}
155
156#[derive(Serialize)]
158pub struct SubmitTransactionResponse {
159 pub transaction_id: String,
161 pub accepted: bool,
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub error: Option<String>,
166}
167
168pub fn consensus_routes() -> Router<AppState> {
170 Router::new()
171 .route("/api/consensus/status", get(get_consensus_status))
172 .route("/api/consensus/blocks", get(list_recent_blocks))
173 .route("/api/consensus/blocks/{height}", get(get_block_by_height))
174 .route("/api/consensus/validators", get(get_validators))
175 .route("/api/consensus/mempool", get(get_mempool_stats))
176 .route("/api/consensus/transactions", post(submit_transaction))
177}
178
179async fn get_consensus_status(State(state): State<AppState>) -> impl IntoResponse {
181 if let Some(ref consensus) = state.consensus {
182 let current_leader = consensus.current_leader().map(|pk| pk.to_hex());
183 let pending_transactions = state.mempool.as_ref().map(|m| m.len()).unwrap_or(0);
184
185 Json(ConsensusStatusResponse {
186 enabled: true,
187 state: format!("{:?}", consensus.state()),
188 view: consensus.view(),
189 finalized_height: consensus.finalized_height(),
190 current_leader,
191 is_leader: consensus.is_leader(),
192 pending_transactions,
193 })
194 } else {
195 Json(ConsensusStatusResponse {
196 enabled: false,
197 state: "Disabled".to_string(),
198 view: 0,
199 finalized_height: 0,
200 current_leader: None,
201 is_leader: false,
202 pending_transactions: 0,
203 })
204 }
205}
206
207async fn list_recent_blocks(State(state): State<AppState>) -> impl IntoResponse {
209 if let Some(ref consensus) = state.consensus {
210 let finalized_height = consensus.finalized_height();
211 let start_height = finalized_height.saturating_sub(9); let mut blocks = Vec::new();
214 for height in start_height..=finalized_height {
215 if let Some(block) = consensus.get_block(height) {
216 blocks.push(BlockInfoResponse {
217 height: block.height(),
218 block_id: block.id().to_hex(),
219 parent_id: block.block.parent().to_hex(),
220 producer: block.block.header.producer.to_hex(),
221 timestamp: block.block.timestamp(),
222 tx_count: block.block.tx_count(),
223 tx_root: hex::encode(block.block.header.tx_root),
224 state_root: hex::encode(block.block.header.state_root),
225 view: block.view,
226 signature_count: block.signature_count(),
227 });
228 }
229 }
230
231 (StatusCode::OK, Json(blocks))
232 } else {
233 (StatusCode::OK, Json(Vec::<BlockInfoResponse>::new()))
234 }
235}
236
237async fn get_block_by_height(
239 State(state): State<AppState>,
240 Path(height): Path<u64>,
241) -> Result<impl IntoResponse, (StatusCode, String)> {
242 if let Some(ref consensus) = state.consensus {
243 if let Some(block) = consensus.get_block(height) {
244 Ok(Json(BlockInfoResponse {
245 height: block.height(),
246 block_id: block.id().to_hex(),
247 parent_id: block.block.parent().to_hex(),
248 producer: block.block.header.producer.to_hex(),
249 timestamp: block.block.timestamp(),
250 tx_count: block.block.tx_count(),
251 tx_root: hex::encode(block.block.header.tx_root),
252 state_root: hex::encode(block.block.header.state_root),
253 view: block.view,
254 signature_count: block.signature_count(),
255 }))
256 } else {
257 Err((
258 StatusCode::NOT_FOUND,
259 format!("Block at height {} not found", height),
260 ))
261 }
262 } else {
263 Err((
264 StatusCode::SERVICE_UNAVAILABLE,
265 "Consensus is not enabled".to_string(),
266 ))
267 }
268}
269
270async fn get_validators(State(state): State<AppState>) -> impl IntoResponse {
272 if let Some(ref consensus) = state.consensus {
273 let validators_lock = consensus.validators();
274 let validators = validators_lock.read();
275
276 let validator_list: Vec<ValidatorInfoResponse> = validators
277 .validators()
278 .iter()
279 .map(|v| ValidatorInfoResponse {
280 name: v.name.clone(),
281 pubkey: v.pubkey.to_hex(),
282 weight: v.weight,
283 addr: v.addr.to_string(),
284 active: v.active,
285 })
286 .collect();
287
288 Json(ValidatorSetResponse {
289 epoch: validators.epoch(),
290 total_weight: validators.total_weight(),
291 quorum_weight: validators.quorum_weight(),
292 validator_count: validators.len(),
293 validators: validator_list,
294 })
295 } else {
296 Json(ValidatorSetResponse {
297 epoch: 0,
298 total_weight: 0,
299 quorum_weight: 0,
300 validator_count: 0,
301 validators: Vec::new(),
302 })
303 }
304}
305
306async fn get_mempool_stats(State(state): State<AppState>) -> impl IntoResponse {
308 if let Some(ref mempool) = state.mempool {
309 let stats = mempool.stats();
310 Json(MempoolStatsResponse {
311 transaction_count: stats.transaction_count,
312 oldest_age_secs: stats.oldest_transaction_age.as_secs_f64(),
313 average_propose_count: stats.average_propose_count,
314 })
315 } else {
316 Json(MempoolStatsResponse {
317 transaction_count: 0,
318 oldest_age_secs: 0.0,
319 average_propose_count: 0.0,
320 })
321 }
322}
323
324async fn submit_transaction(
326 State(state): State<AppState>,
327 Json(req): Json<SubmitTransactionRequest>,
328) -> Result<impl IntoResponse, (StatusCode, Json<SubmitTransactionResponse>)> {
329 let transaction = match req {
331 SubmitTransactionRequest::CreateRepository {
332 owner,
333 name,
334 description,
335 default_branch,
336 visibility,
337 creator_pubkey,
338 signature,
339 } => Transaction::CreateRepository {
340 owner,
341 name,
342 description,
343 default_branch,
344 visibility,
345 creator: SerializablePublicKey::from_hex(&creator_pubkey),
346 signature: SerializableSignature::from_hex(&signature),
347 },
348 SubmitTransactionRequest::CreateIssue {
349 repo_key,
350 title,
351 body,
352 author,
353 creator_pubkey,
354 signature,
355 } => Transaction::CreateIssue {
356 repo_key,
357 title,
358 description: body,
359 author,
360 signer: SerializablePublicKey::from_hex(&creator_pubkey),
361 signature: SerializableSignature::from_hex(&signature),
362 },
363 SubmitTransactionRequest::CreatePullRequest {
364 repo_key,
365 title,
366 description,
367 author,
368 source_branch,
369 target_branch,
370 source_commit,
371 target_commit,
372 creator_pubkey,
373 signature,
374 } => {
375 let source_oid = guts_storage::ObjectId::from_hex(&source_commit).map_err(|_| {
377 (
378 StatusCode::BAD_REQUEST,
379 Json(SubmitTransactionResponse {
380 transaction_id: String::new(),
381 accepted: false,
382 error: Some("Invalid source_commit hex".to_string()),
383 }),
384 )
385 })?;
386 let target_oid = guts_storage::ObjectId::from_hex(&target_commit).map_err(|_| {
387 (
388 StatusCode::BAD_REQUEST,
389 Json(SubmitTransactionResponse {
390 transaction_id: String::new(),
391 accepted: false,
392 error: Some("Invalid target_commit hex".to_string()),
393 }),
394 )
395 })?;
396
397 Transaction::CreatePullRequest {
398 repo_key,
399 title,
400 description,
401 author,
402 source_branch,
403 target_branch,
404 source_commit: source_oid,
405 target_commit: target_oid,
406 signer: SerializablePublicKey::from_hex(&creator_pubkey),
407 signature: SerializableSignature::from_hex(&signature),
408 }
409 }
410 };
411
412 if let Some(ref consensus) = state.consensus {
414 match consensus.submit_transaction(transaction.clone()).await {
415 Ok(id) => Ok((
416 StatusCode::ACCEPTED,
417 Json(SubmitTransactionResponse {
418 transaction_id: id.to_hex(),
419 accepted: true,
420 error: None,
421 }),
422 )),
423 Err(e) => Err((
424 StatusCode::BAD_REQUEST,
425 Json(SubmitTransactionResponse {
426 transaction_id: String::new(),
427 accepted: false,
428 error: Some(e.to_string()),
429 }),
430 )),
431 }
432 } else if let Some(ref mempool) = state.mempool {
433 match mempool.add(transaction) {
434 Ok(id) => Ok((
435 StatusCode::ACCEPTED,
436 Json(SubmitTransactionResponse {
437 transaction_id: id.to_hex(),
438 accepted: true,
439 error: None,
440 }),
441 )),
442 Err(e) => Err((
443 StatusCode::BAD_REQUEST,
444 Json(SubmitTransactionResponse {
445 transaction_id: String::new(),
446 accepted: false,
447 error: Some(e.to_string()),
448 }),
449 )),
450 }
451 } else {
452 Err((
453 StatusCode::SERVICE_UNAVAILABLE,
454 Json(SubmitTransactionResponse {
455 transaction_id: String::new(),
456 accepted: false,
457 error: Some("Consensus and mempool are not enabled".to_string()),
458 }),
459 ))
460 }
461}
462
463#[cfg(test)]
464mod tests {
465 use super::*;
466
467 #[test]
468 fn test_consensus_status_response_serialization() {
469 let response = ConsensusStatusResponse {
470 enabled: true,
471 state: "Active".to_string(),
472 view: 10,
473 finalized_height: 100,
474 current_leader: Some("abc123".to_string()),
475 is_leader: false,
476 pending_transactions: 5,
477 };
478
479 let json = serde_json::to_string(&response).unwrap();
480 assert!(json.contains("\"enabled\":true"));
481 assert!(json.contains("\"view\":10"));
482 }
483
484 #[test]
485 fn test_block_info_response_serialization() {
486 let response = BlockInfoResponse {
487 height: 42,
488 block_id: "abc".to_string(),
489 parent_id: "def".to_string(),
490 producer: "xyz".to_string(),
491 timestamp: 1234567890,
492 tx_count: 10,
493 tx_root: "root".to_string(),
494 state_root: "state".to_string(),
495 view: 5,
496 signature_count: 3,
497 };
498
499 let json = serde_json::to_string(&response).unwrap();
500 assert!(json.contains("\"height\":42"));
501 assert!(json.contains("\"tx_count\":10"));
502 }
503}