Skip to content

Commit 474d3f1

Browse files
committed
feat(phase4): implement RPC methods for block explorer and metrics
1 parent 8c4ffe8 commit 474d3f1

1 file changed

Lines changed: 177 additions & 0 deletions

File tree

crates/bitcell-node/src/rpc.rs

Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ async fn handle_json_rpc(
8686
let result = match req.method.as_str() {
8787
// Standard Namespace
8888
"eth_blockNumber" => eth_block_number(&state).await,
89+
"eth_getBlockByNumber" => eth_get_block_by_number(&state, req.params).await,
90+
"eth_getTransactionByHash" => eth_get_transaction_by_hash(&state, req.params).await,
8991
"eth_getBalance" => eth_get_balance(&state, req.params).await,
9092
"eth_sendRawTransaction" => eth_send_raw_transaction(&state, req.params).await,
9193

@@ -131,6 +133,181 @@ async fn eth_block_number(state: &RpcState) -> Result<Value, JsonRpcError> {
131133
Ok(json!(format!("0x{:x}", height)))
132134
}
133135

136+
async fn eth_get_block_by_number(state: &RpcState, params: Option<Value>) -> Result<Value, JsonRpcError> {
137+
let params = params.ok_or(JsonRpcError {
138+
code: -32602,
139+
message: "Invalid params".to_string(),
140+
data: None,
141+
})?;
142+
143+
let args = params.as_array().ok_or(JsonRpcError {
144+
code: -32602,
145+
message: "Params must be an array".to_string(),
146+
data: None,
147+
})?;
148+
149+
if args.is_empty() {
150+
return Err(JsonRpcError {
151+
code: -32602,
152+
message: "Missing block number".to_string(),
153+
data: None,
154+
});
155+
}
156+
157+
let block_param = args[0].as_str().ok_or(JsonRpcError {
158+
code: -32602,
159+
message: "Block number must be a string".to_string(),
160+
data: None,
161+
})?;
162+
163+
let include_txs = if args.len() > 1 {
164+
args[1].as_bool().unwrap_or(false)
165+
} else {
166+
false
167+
};
168+
169+
let height = if block_param == "latest" {
170+
state.blockchain.height()
171+
} else if block_param == "earliest" {
172+
0
173+
} else if block_param == "pending" {
174+
state.blockchain.height() // TODO: Support pending block
175+
} else {
176+
let hex = block_param.strip_prefix("0x").unwrap_or(block_param);
177+
u64::from_str_radix(hex, 16).map_err(|_| JsonRpcError {
178+
code: -32602,
179+
message: "Invalid block number format".to_string(),
180+
data: None,
181+
})?
182+
};
183+
184+
if let Some(block) = state.blockchain.get_block(height) {
185+
let transactions = if include_txs {
186+
let txs: Vec<Value> = block.transactions.iter().enumerate().map(|(i, tx)| {
187+
json!({
188+
"hash": format!("0x{}", hex::encode(tx.hash().as_bytes())),
189+
"nonce": format!("0x{:x}", tx.nonce),
190+
"blockHash": format!("0x{}", hex::encode(block.hash().as_bytes())),
191+
"blockNumber": format!("0x{:x}", block.header.height),
192+
"transactionIndex": format!("0x{:x}", i),
193+
"from": format!("0x{}", hex::encode(tx.from.as_bytes())),
194+
"to": format!("0x{}", hex::encode(tx.to.as_bytes())),
195+
"value": format!("0x{:x}", tx.amount),
196+
"gas": format!("0x{:x}", tx.gas_limit),
197+
"gasPrice": format!("0x{:x}", tx.gas_price),
198+
"input": format!("0x{}", hex::encode(&tx.data)),
199+
})
200+
}).collect();
201+
json!(txs)
202+
} else {
203+
let tx_hashes: Vec<String> = block.transactions.iter()
204+
.map(|tx| format!("0x{}", hex::encode(tx.hash().as_bytes())))
205+
.collect();
206+
json!(tx_hashes)
207+
};
208+
209+
Ok(json!({
210+
"number": format!("0x{:x}", block.header.height),
211+
"hash": format!("0x{}", hex::encode(block.hash().as_bytes())),
212+
"parentHash": format!("0x{}", hex::encode(block.header.prev_hash.as_bytes())),
213+
"nonce": "0x0000000000000000", // TODO: Use work/nonce
214+
"sha3Uncles": "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", // Empty uncle hash
215+
"logsBloom": "0x00", // TODO: Bloom filter
216+
"transactionsRoot": format!("0x{}", hex::encode(block.header.tx_root.as_bytes())),
217+
"stateRoot": format!("0x{}", hex::encode(block.header.state_root.as_bytes())),
218+
"receiptsRoot": "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", // Empty receipts root
219+
"miner": format!("0x{}", hex::encode(block.header.proposer.as_bytes())),
220+
"difficulty": "0x1",
221+
"totalDifficulty": format!("0x{:x}", block.header.height), // Simplified
222+
"extraData": "0x",
223+
"size": format!("0x{:x}", 1000), // TODO: Real size
224+
"gasLimit": "0x1fffffffffffff",
225+
"gasUsed": "0x0",
226+
"timestamp": format!("0x{:x}", block.header.timestamp),
227+
"transactions": transactions,
228+
"uncles": []
229+
}))
230+
} else {
231+
Ok(Value::Null)
232+
}
233+
}
234+
235+
async fn eth_get_transaction_by_hash(state: &RpcState, params: Option<Value>) -> Result<Value, JsonRpcError> {
236+
let params = params.ok_or(JsonRpcError {
237+
code: -32602,
238+
message: "Invalid params".to_string(),
239+
data: None,
240+
})?;
241+
242+
let args = params.as_array().ok_or(JsonRpcError {
243+
code: -32602,
244+
message: "Params must be an array".to_string(),
245+
data: None,
246+
})?;
247+
248+
if args.is_empty() {
249+
return Err(JsonRpcError {
250+
code: -32602,
251+
message: "Missing transaction hash".to_string(),
252+
data: None,
253+
});
254+
}
255+
256+
let tx_hash_str = args[0].as_str().ok_or(JsonRpcError {
257+
code: -32602,
258+
message: "Transaction hash must be a string".to_string(),
259+
data: None,
260+
})?;
261+
262+
let tx_hash_hex = tx_hash_str.strip_prefix("0x").unwrap_or(tx_hash_str);
263+
let tx_hash_bytes = hex::decode(tx_hash_hex).map_err(|_| JsonRpcError {
264+
code: -32602,
265+
message: "Invalid hex encoding".to_string(),
266+
data: None,
267+
})?;
268+
269+
if tx_hash_bytes.len() != 32 {
270+
return Err(JsonRpcError {
271+
code: -32602,
272+
message: "Transaction hash must be 32 bytes".to_string(),
273+
data: None,
274+
});
275+
}
276+
277+
let mut hash = [0u8; 32];
278+
hash.copy_from_slice(&tx_hash_bytes);
279+
let target_hash = bitcell_crypto::Hash256::from(hash);
280+
281+
// Search in blockchain (inefficient linear scan for now, need index later)
282+
let height = state.blockchain.height();
283+
// Scan last 100 blocks for efficiency in this demo
284+
let start_height = if height > 100 { height - 100 } else { 0 };
285+
286+
for h in (start_height..=height).rev() {
287+
if let Some(block) = state.blockchain.get_block(h) {
288+
for (i, tx) in block.transactions.iter().enumerate() {
289+
if tx.hash() == target_hash {
290+
return Ok(json!({
291+
"hash": format!("0x{}", hex::encode(tx.hash().as_bytes())),
292+
"nonce": format!("0x{:x}", tx.nonce),
293+
"blockHash": format!("0x{}", hex::encode(block.hash().as_bytes())),
294+
"blockNumber": format!("0x{:x}", block.header.height),
295+
"transactionIndex": format!("0x{:x}", i),
296+
"from": format!("0x{}", hex::encode(tx.from.as_bytes())),
297+
"to": format!("0x{}", hex::encode(tx.to.as_bytes())),
298+
"value": format!("0x{:x}", tx.amount),
299+
"gas": format!("0x{:x}", tx.gas_limit),
300+
"gasPrice": format!("0x{:x}", tx.gas_price),
301+
"input": format!("0x{}", hex::encode(&tx.data)),
302+
}));
303+
}
304+
}
305+
}
306+
}
307+
308+
Ok(Value::Null)
309+
}
310+
134311
async fn eth_get_balance(state: &RpcState, params: Option<Value>) -> Result<Value, JsonRpcError> {
135312
let params = params.ok_or(JsonRpcError {
136313
code: -32602,

0 commit comments

Comments
 (0)