|
1 | | -//! DevilChain GraphQL API |
| 1 | +//! DevilChain GraphQL API — async-graphql |
2 | 2 | //! Runs on port 8546 alongside REST (8545) |
3 | 3 |
|
4 | | -use async_graphql::*; |
| 4 | +use async_graphql::{Object, Schema, EmptyMutation, EmptySubscription, SimpleObject}; |
| 5 | +use async_graphql_axum::{GraphQLRequest, GraphQLResponse}; |
| 6 | +use axum::{extract::State, routing::post, Router}; |
5 | 7 | use std::sync::Arc; |
6 | 8 | use tokio::sync::RwLock; |
7 | 9 | use crate::blockchain::Blockchain; |
8 | 10 |
|
9 | | -// --- GraphQL Types --- |
| 11 | +#[derive(SimpleObject, Clone)] |
| 12 | +pub struct BlockGQL { |
| 13 | + pub height: i64, |
| 14 | + pub hash: String, |
| 15 | + pub timestamp: i64, |
| 16 | + pub validator: String, |
| 17 | + pub merkle_root: String, |
| 18 | + pub nonce: i64, |
| 19 | + pub ai_score: f64, |
| 20 | + pub dao_signature: String, |
| 21 | + pub tx_count: i32, |
| 22 | +} |
10 | 23 |
|
11 | 24 | #[derive(SimpleObject, Clone)] |
12 | | -pub struct GqlTransaction { |
| 25 | +pub struct TransactionGQL { |
13 | 26 | pub tx_hash: String, |
14 | 27 | pub from: String, |
15 | 28 | pub to: String, |
16 | 29 | pub amount: f64, |
17 | 30 | pub gas_fee: f64, |
18 | | - pub timestamp: u64, |
19 | | - pub signature: String, |
| 31 | + pub timestamp: i64, |
20 | 32 | } |
21 | 33 |
|
22 | 34 | #[derive(SimpleObject, Clone)] |
23 | | -pub struct GqlBlock { |
24 | | - pub block_height: u64, |
25 | | - pub timestamp: u64, |
26 | | - pub previous_hash: String, |
27 | | - pub validator: String, |
28 | | - pub merkle_root: String, |
29 | | - pub nonce: u64, |
30 | | - pub ai_score: f64, |
31 | | - pub dao_signature: String, |
32 | | - pub block_hash: String, |
33 | | - pub transactions: Vec<GqlTransaction>, |
| 35 | +pub struct NetworkStatus { |
| 36 | + pub network: String, |
| 37 | + pub coin: String, |
| 38 | + pub symbol: String, |
| 39 | + pub chain_length: i64, |
| 40 | + pub latest_height: i64, |
| 41 | + pub tps_target: String, |
| 42 | + pub block_time: String, |
| 43 | + pub consensus: String, |
34 | 44 | } |
35 | 45 |
|
36 | | -pub struct QueryRoot; |
| 46 | +pub struct QueryRoot { |
| 47 | + pub blockchain: Arc<RwLock<Blockchain>>, |
| 48 | +} |
37 | 49 |
|
38 | 50 | #[Object] |
39 | 51 | impl QueryRoot { |
40 | | - /// Get the latest block |
41 | | - async fn latest_block(&self, ctx: &Context<'_>) -> Result<GqlBlock> { |
42 | | - let bc = ctx.data::<Arc<RwLock<Blockchain>>>()?; |
43 | | - let chain = bc.read().await; |
44 | | - let b = chain.latest_block(); |
45 | | - Ok(to_gql_block(b)) |
| 52 | + async fn latest_block(&self) -> BlockGQL { |
| 53 | + let bc = self.blockchain.read().await; |
| 54 | + let b = bc.latest_block(); |
| 55 | + BlockGQL { |
| 56 | + height: b.block_height as i64, |
| 57 | + hash: b.block_hash.clone(), |
| 58 | + timestamp: b.timestamp as i64, |
| 59 | + validator: b.validator.clone(), |
| 60 | + merkle_root: b.merkle_root.clone(), |
| 61 | + nonce: b.nonce as i64, |
| 62 | + ai_score: b.ai_score, |
| 63 | + dao_signature: b.dao_signature.clone(), |
| 64 | + tx_count: b.transactions.len() as i32, |
| 65 | + } |
46 | 66 | } |
47 | 67 |
|
48 | | - /// Get block by height |
49 | | - async fn block(&self, ctx: &Context<'_>, height: u64) -> Result<Option<GqlBlock>> { |
50 | | - let bc = ctx.data::<Arc<RwLock<Blockchain>>>()?; |
51 | | - let chain = bc.read().await; |
52 | | - Ok(chain.chain.iter().find(|b| b.block_height == height).map(to_gql_block)) |
| 68 | + async fn block(&self, height: i64) -> Option<BlockGQL> { |
| 69 | + let bc = self.blockchain.read().await; |
| 70 | + bc.chain.iter().find(|b| b.block_height == height as u64).map(|b| BlockGQL { |
| 71 | + height: b.block_height as i64, |
| 72 | + hash: b.block_hash.clone(), |
| 73 | + timestamp: b.timestamp as i64, |
| 74 | + validator: b.validator.clone(), |
| 75 | + merkle_root: b.merkle_root.clone(), |
| 76 | + nonce: b.nonce as i64, |
| 77 | + ai_score: b.ai_score, |
| 78 | + dao_signature: b.dao_signature.clone(), |
| 79 | + tx_count: b.transactions.len() as i32, |
| 80 | + }) |
53 | 81 | } |
54 | 82 |
|
55 | | - /// Get network status |
56 | | - async fn status(&self, ctx: &Context<'_>) -> Result<String> { |
57 | | - let bc = ctx.data::<Arc<RwLock<Blockchain>>>()?; |
58 | | - let chain = bc.read().await; |
59 | | - Ok(format!("DevilChain | Height: {} | TPS: 5000-20000", chain.latest_block().block_height)) |
| 83 | + async fn status(&self) -> NetworkStatus { |
| 84 | + let bc = self.blockchain.read().await; |
| 85 | + NetworkStatus { |
| 86 | + network: "DevilChain".to_string(), |
| 87 | + coin: "DevilCoin (DVC)".to_string(), |
| 88 | + symbol: "DVL".to_string(), |
| 89 | + chain_length: bc.chain.len() as i64, |
| 90 | + latest_height: bc.latest_block().block_height as i64, |
| 91 | + tps_target: "5000-20000".to_string(), |
| 92 | + block_time: "2-5s".to_string(), |
| 93 | + consensus: "Devil Hybrid Protocol (DHP)".to_string(), |
| 94 | + } |
60 | 95 | } |
61 | 96 |
|
62 | | - /// Get all blocks (paginated) |
63 | | - async fn blocks(&self, ctx: &Context<'_>, limit: Option<u64>, offset: Option<u64>) -> Result<Vec<GqlBlock>> { |
64 | | - let bc = ctx.data::<Arc<RwLock<Blockchain>>>()?; |
65 | | - let chain = bc.read().await; |
66 | | - let offset = offset.unwrap_or(0) as usize; |
67 | | - let limit = limit.unwrap_or(10) as usize; |
68 | | - let blocks = chain.chain.iter().rev().skip(offset).take(limit).map(to_gql_block).collect(); |
69 | | - Ok(blocks) |
70 | | - } |
71 | | -} |
72 | | - |
73 | | -fn to_gql_block(b: &crate::blockchain::Block) -> GqlBlock { |
74 | | - GqlBlock { |
75 | | - block_height: b.block_height, |
76 | | - timestamp: b.timestamp, |
77 | | - previous_hash: b.previous_hash.clone(), |
78 | | - validator: b.validator.clone(), |
79 | | - merkle_root: b.merkle_root.clone(), |
80 | | - nonce: b.nonce, |
81 | | - ai_score: b.ai_score, |
82 | | - dao_signature: b.dao_signature.clone(), |
83 | | - block_hash: b.block_hash.clone(), |
84 | | - transactions: b.transactions.iter().map(|tx| GqlTransaction { |
85 | | - tx_hash: tx.tx_hash.clone(), |
86 | | - from: tx.from.clone(), |
87 | | - to: tx.to.clone(), |
88 | | - amount: tx.amount, |
89 | | - gas_fee: tx.gas_fee, |
90 | | - timestamp: tx.timestamp, |
91 | | - signature: tx.signature.clone(), |
92 | | - }).collect(), |
| 97 | + async fn transaction(&self, tx_hash: String) -> Option<TransactionGQL> { |
| 98 | + let bc = self.blockchain.read().await; |
| 99 | + for block in &bc.chain { |
| 100 | + for tx in &block.transactions { |
| 101 | + if tx.tx_hash == tx_hash { |
| 102 | + return Some(TransactionGQL { |
| 103 | + tx_hash: tx.tx_hash.clone(), |
| 104 | + from: tx.from.clone(), |
| 105 | + to: tx.to.clone(), |
| 106 | + amount: tx.amount, |
| 107 | + gas_fee: tx.gas_fee, |
| 108 | + timestamp: tx.timestamp as i64, |
| 109 | + }); |
| 110 | + } |
| 111 | + } |
| 112 | + } |
| 113 | + None |
93 | 114 | } |
94 | 115 | } |
95 | 116 |
|
96 | 117 | pub type DevilSchema = Schema<QueryRoot, EmptyMutation, EmptySubscription>; |
97 | 118 |
|
98 | 119 | pub fn build_schema(blockchain: Arc<RwLock<Blockchain>>) -> DevilSchema { |
99 | | - Schema::build(QueryRoot, EmptyMutation, EmptySubscription) |
100 | | - .data(blockchain) |
101 | | - .finish() |
| 120 | + Schema::build(QueryRoot { blockchain }, EmptyMutation, EmptySubscription).finish() |
102 | 121 | } |
103 | 122 |
|
104 | | -pub async fn start_graphql_server(blockchain: Arc<RwLock<Blockchain>>) { |
105 | | - use async_graphql_axum::{GraphQL, GraphQLSubscription}; |
106 | | - use axum::{Router, routing::get}; |
| 123 | +pub async fn graphql_handler( |
| 124 | + State(schema): State<DevilSchema>, |
| 125 | + req: GraphQLRequest, |
| 126 | +) -> GraphQLResponse { |
| 127 | + schema.execute(req.into_inner()).await.into() |
| 128 | +} |
107 | 129 |
|
| 130 | +pub async fn start_graphql_server(blockchain: Arc<RwLock<Blockchain>>) { |
108 | 131 | let schema = build_schema(blockchain); |
109 | 132 | let app = Router::new() |
110 | | - .route("/graphql", get(graphql_playground).post_service(GraphQL::new(schema.clone()))); |
111 | | - |
| 133 | + .route("/graphql", post(graphql_handler)) |
| 134 | + .with_state(schema); |
112 | 135 | let listener = tokio::net::TcpListener::bind("0.0.0.0:8546").await.unwrap(); |
113 | | - log::info!("GraphQL Playground: http://localhost:8546/graphql"); |
| 136 | + log::info!("GraphQL API listening on :8546/graphql"); |
114 | 137 | axum::serve(listener, app).await.unwrap(); |
115 | 138 | } |
116 | | - |
117 | | -async fn graphql_playground() -> impl axum::response::IntoResponse { |
118 | | - axum::response::Html(async_graphql::http::playground_source( |
119 | | - async_graphql::http::GraphQLPlaygroundConfig::new("/graphql"), |
120 | | - )) |
121 | | -} |
|
0 commit comments