1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::log_template::LogTemplate;
9use crate::protocol::EventType;
10use crate::state::app_state::AppState;
11use anyhow::{Context, Result};
12use serde_json::json;
13use std::sync::LazyLock;
14
15pub static BITCOIN_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
17 EventType::new(
18 "bitcoin_connected",
19 "Bitcoin RPC client initialized and ready to execute commands",
20 json!({"type": "execute_rpc", "method": "getblockchaininfo", "params": []}),
21 )
22 .with_parameters(vec![Parameter {
23 name: "rpc_url".to_string(),
24 type_hint: "string".to_string(),
25 description: "Bitcoin Core RPC URL".to_string(),
26 required: true,
27 }])
28});
29
30pub static BITCOIN_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
32 EventType::new(
33 "bitcoin_response_received",
34 "Response received from Bitcoin Core RPC",
35 json!({"type": "execute_rpc", "method": "getblock", "params": ["00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"]}),
36 )
37 .with_parameters(vec![
38 Parameter {
39 name: "method".to_string(),
40 type_hint: "string".to_string(),
41 description: "RPC method that was called".to_string(),
42 required: true,
43 },
44 Parameter {
45 name: "result".to_string(),
46 type_hint: "any".to_string(),
47 description: "The result from the RPC call".to_string(),
48 required: false,
49 },
50 Parameter {
51 name: "error".to_string(),
52 type_hint: "any".to_string(),
53 description: "Error message if the RPC call failed".to_string(),
54 required: false,
55 },
56 Parameter {
57 name: "status_code".to_string(),
58 type_hint: "number".to_string(),
59 description: "HTTP status code".to_string(),
60 required: true,
61 },
62 ])
63});
64
65pub struct BitcoinClientProtocol;
67
68impl BitcoinClientProtocol {
69 pub fn new() -> Self {
70 Self
71 }
72}
73
74impl Protocol for BitcoinClientProtocol {
76 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
77 vec![
78 ParameterDefinition {
79 name: "rpc_user".to_string(),
80 description: "Bitcoin RPC username".to_string(),
81 type_hint: "string".to_string(),
82 required: false,
83 example: json!("bitcoinrpc"),
84 default: None,
85 },
86 ParameterDefinition {
87 name: "rpc_password".to_string(),
88 description: "Bitcoin RPC password".to_string(),
89 type_hint: "string".to_string(),
90 required: false,
91 example: json!("password123"),
92 default: None,
93 },
94 ]
95 }
96 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
97 vec![
98 ActionDefinition {
100 name: "get_blockchain_info".to_string(),
101 description: "Get blockchain information (chain, blocks, difficulty, etc.)"
102 .to_string(),
103 parameters: vec![],
104 example: json!({
105 "type": "get_blockchain_info"
106 }),
107 log_template: None,
108 },
109 ActionDefinition {
110 name: "get_block_hash".to_string(),
111 description: "Get block hash by height".to_string(),
112 parameters: vec![Parameter {
113 name: "height".to_string(),
114 type_hint: "number".to_string(),
115 description: "Block height".to_string(),
116 required: true,
117 }],
118 example: json!({
119 "type": "get_block_hash",
120 "height": 700000
121 }),
122 log_template: None,
123 },
124 ActionDefinition {
125 name: "get_block".to_string(),
126 description: "Get block details by hash".to_string(),
127 parameters: vec![
128 Parameter {
129 name: "block_hash".to_string(),
130 type_hint: "string".to_string(),
131 description: "Block hash (hex)".to_string(),
132 required: true,
133 },
134 Parameter {
135 name: "verbosity".to_string(),
136 type_hint: "number".to_string(),
137 description: "Verbosity level (0=hex, 1=json, 2=json with tx details)"
138 .to_string(),
139 required: false,
140 },
141 ],
142 example: json!({
143 "type": "get_block",
144 "block_hash": "00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048",
145 "verbosity": 1
146 }),
147 log_template: None,
148 },
149 ActionDefinition {
150 name: "get_transaction".to_string(),
151 description: "Get transaction details by txid".to_string(),
152 parameters: vec![Parameter {
153 name: "txid".to_string(),
154 type_hint: "string".to_string(),
155 description: "Transaction ID (hex)".to_string(),
156 required: true,
157 }],
158 example: json!({
159 "type": "get_transaction",
160 "txid": "f4184fc596403b9d638783cf57adfe4c75c605f6356fbc91338530e9831e9e16"
161 }),
162 log_template: None,
163 },
164 ActionDefinition {
165 name: "get_mempool_info".to_string(),
166 description: "Get mempool information (size, bytes, usage, etc.)".to_string(),
167 parameters: vec![],
168 example: json!({
169 "type": "get_mempool_info"
170 }),
171 log_template: None,
172 },
173 ActionDefinition {
174 name: "get_raw_mempool".to_string(),
175 description: "Get list of transaction IDs in mempool".to_string(),
176 parameters: vec![Parameter {
177 name: "verbose".to_string(),
178 type_hint: "boolean".to_string(),
179 description: "Return verbose object for each transaction".to_string(),
180 required: false,
181 }],
182 example: json!({
183 "type": "get_raw_mempool",
184 "verbose": false
185 }),
186 log_template: None,
187 },
188 ActionDefinition {
189 name: "get_mining_info".to_string(),
190 description: "Get mining information (network hashrate, difficulty, etc.)"
191 .to_string(),
192 parameters: vec![],
193 example: json!({
194 "type": "get_mining_info"
195 }),
196 log_template: None,
197 },
198 ActionDefinition {
200 name: "get_network_info".to_string(),
201 description: "Get network information (version, connections, protocols, etc.)"
202 .to_string(),
203 parameters: vec![],
204 example: json!({
205 "type": "get_network_info"
206 }),
207 log_template: None,
208 },
209 ActionDefinition {
210 name: "get_peer_info".to_string(),
211 description: "Get information about connected peers".to_string(),
212 parameters: vec![],
213 example: json!({
214 "type": "get_peer_info"
215 }),
216 log_template: None,
217 },
218 ActionDefinition {
219 name: "get_connection_count".to_string(),
220 description: "Get number of connections to other nodes".to_string(),
221 parameters: vec![],
222 example: json!({
223 "type": "get_connection_count"
224 }),
225 log_template: None,
226 },
227 ActionDefinition {
229 name: "get_wallet_info".to_string(),
230 description: "Get wallet information (balance, txcount, etc.)".to_string(),
231 parameters: vec![],
232 example: json!({
233 "type": "get_wallet_info"
234 }),
235 log_template: None,
236 },
237 ActionDefinition {
238 name: "get_balance".to_string(),
239 description: "Get wallet balance".to_string(),
240 parameters: vec![],
241 example: json!({
242 "type": "get_balance"
243 }),
244 log_template: None,
245 },
246 ActionDefinition {
247 name: "list_transactions".to_string(),
248 description: "List wallet transactions".to_string(),
249 parameters: vec![Parameter {
250 name: "count".to_string(),
251 type_hint: "number".to_string(),
252 description: "Number of transactions to list".to_string(),
253 required: false,
254 }],
255 example: json!({
256 "type": "list_transactions",
257 "count": 10
258 }),
259 log_template: None,
260 },
261 ActionDefinition {
263 name: "execute_rpc".to_string(),
264 description: "Execute a custom Bitcoin RPC command".to_string(),
265 parameters: vec![
266 Parameter {
267 name: "method".to_string(),
268 type_hint: "string".to_string(),
269 description: "RPC method name".to_string(),
270 required: true,
271 },
272 Parameter {
273 name: "params".to_string(),
274 type_hint: "array".to_string(),
275 description: "RPC parameters (array)".to_string(),
276 required: false,
277 },
278 ],
279 example: json!({
280 "type": "execute_rpc",
281 "method": "getbestblockhash",
282 "params": []
283 }),
284 log_template: None,
285 },
286 ActionDefinition {
287 name: "disconnect".to_string(),
288 description: "Disconnect from the Bitcoin RPC server".to_string(),
289 parameters: vec![],
290 example: json!({
291 "type": "disconnect"
292 }),
293 log_template: None,
294 },
295 ]
296 }
297 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
298 vec![ActionDefinition {
299 name: "execute_rpc".to_string(),
300 description: "Execute another RPC command in response to received data".to_string(),
301 parameters: vec![
302 Parameter {
303 name: "method".to_string(),
304 type_hint: "string".to_string(),
305 description: "RPC method name".to_string(),
306 required: true,
307 },
308 Parameter {
309 name: "params".to_string(),
310 type_hint: "array".to_string(),
311 description: "RPC parameters".to_string(),
312 required: false,
313 },
314 ],
315 example: json!({
316 "type": "execute_rpc",
317 "method": "getblock",
318 "params": ["00000000839a8e6886ab5951d76f411475428afc90947ee320161bbf18eb6048"]
319 }),
320 log_template: Some(
321 LogTemplate::new()
322 .with_info("-> Bitcoin RPC {method}")
323 .with_debug("Bitcoin execute_rpc: method={method}"),
324 ),
325 }]
326 }
327 fn protocol_name(&self) -> &'static str {
328 "Bitcoin"
329 }
330 fn get_event_types(&self) -> Vec<EventType> {
338 vec![
339 BITCOIN_CLIENT_CONNECTED_EVENT.clone(),
340 BITCOIN_CLIENT_RESPONSE_RECEIVED_EVENT.clone(),
341 ]
342 }
343 fn stack_name(&self) -> &'static str {
344 "ETH>IP>TCP>HTTP>JSON-RPC>Bitcoin"
345 }
346 fn keywords(&self) -> Vec<&'static str> {
347 vec![
348 "bitcoin",
349 "btc",
350 "bitcoin rpc",
351 "bitcoin client",
352 "blockchain",
353 ]
354 }
355 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
356 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
357
358 ProtocolMetadataV2::builder()
359 .state(DevelopmentState::Experimental)
360 .implementation("JSON-RPC over HTTP to Bitcoin Core")
361 .llm_control(
362 "Full control over Bitcoin RPC commands (blockchain, wallet, network queries)",
363 )
364 .e2e_testing("Bitcoin Core regtest node or testnet")
365 .build()
366 }
367 fn description(&self) -> &'static str {
368 "Bitcoin RPC client for blockchain queries and wallet operations"
369 }
370 fn example_prompt(&self) -> &'static str {
371 "Connect to Bitcoin Core at http://user:pass@localhost:8332 and get blockchain info"
372 }
373 fn group_name(&self) -> &'static str {
374 "Blockchain"
375 }
376 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
377 use crate::llm::actions::StartupExamples;
378 use serde_json::json;
379
380 StartupExamples::new(
381 json!({
383 "type": "open_client",
384 "remote_addr": "localhost:8332",
385 "base_stack": "bitcoin",
386 "instruction": "Connect to Bitcoin Core and get blockchain info",
387 "startup_params": {
388 "rpc_user": "bitcoinrpc",
389 "rpc_password": "password"
390 }
391 }),
392 json!({
394 "type": "open_client",
395 "remote_addr": "localhost:8332",
396 "base_stack": "bitcoin",
397 "startup_params": {
398 "rpc_user": "bitcoinrpc",
399 "rpc_password": "password"
400 },
401 "event_handlers": [{
402 "event_pattern": "bitcoin_connected",
403 "handler": {
404 "type": "script",
405 "language": "python",
406 "code": "<bitcoin_client_handler>"
407 }
408 }]
409 }),
410 json!({
412 "type": "open_client",
413 "remote_addr": "localhost:8332",
414 "base_stack": "bitcoin",
415 "startup_params": {
416 "rpc_user": "bitcoinrpc",
417 "rpc_password": "password"
418 },
419 "event_handlers": [{
420 "event_pattern": "bitcoin_connected",
421 "handler": {
422 "type": "static",
423 "actions": [{
424 "type": "get_blockchain_info"
425 }]
426 }
427 }]
428 }),
429 )
430 }
431}
432
433impl Client for BitcoinClientProtocol {
435 fn connect(
436 &self,
437 ctx: crate::protocol::ConnectContext,
438 ) -> std::pin::Pin<
439 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
440 > {
441 Box::pin(async move {
442 use crate::client::bitcoin::BitcoinClient;
443 let rpc_user = ctx
447 .startup_params
448 .as_ref()
449 .map(|p| p.get_optional_string("rpc_user"))
450 .transpose()?
451 .flatten();
452 let rpc_password = ctx
453 .startup_params
454 .as_ref()
455 .map(|p| p.get_optional_string("rpc_password"))
456 .transpose()?
457 .flatten();
458
459 BitcoinClient::connect_with_llm_actions(
460 ctx.remote_addr,
461 rpc_user,
462 rpc_password,
463 ctx.llm_client,
464 ctx.state,
465 ctx.status_tx,
466 ctx.client_id,
467 )
468 .await
469 })
470 }
471 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
472 let action_type = action
473 .get("type")
474 .and_then(|v| v.as_str())
475 .context("Missing 'type' field in action")?;
476
477 match action_type {
478 "get_blockchain_info" => Ok(ClientActionResult::Custom {
479 name: "bitcoin_rpc".to_string(),
480 data: json!({
481 "method": "getblockchaininfo",
482 "params": []
483 }),
484 }),
485 "get_block_hash" => {
486 let height = action
487 .get("height")
488 .and_then(|v| v.as_u64())
489 .context("Missing or invalid 'height' field")?;
490
491 Ok(ClientActionResult::Custom {
492 name: "bitcoin_rpc".to_string(),
493 data: json!({
494 "method": "getblockhash",
495 "params": [height]
496 }),
497 })
498 }
499 "get_block" => {
500 let block_hash = action
501 .get("block_hash")
502 .and_then(|v| v.as_str())
503 .context("Missing 'block_hash' field")?;
504
505 let verbosity = action
506 .get("verbosity")
507 .and_then(|v| v.as_u64())
508 .unwrap_or(1);
509
510 Ok(ClientActionResult::Custom {
511 name: "bitcoin_rpc".to_string(),
512 data: json!({
513 "method": "getblock",
514 "params": [block_hash, verbosity]
515 }),
516 })
517 }
518 "get_transaction" => {
519 let txid = action
520 .get("txid")
521 .and_then(|v| v.as_str())
522 .context("Missing 'txid' field")?;
523
524 Ok(ClientActionResult::Custom {
525 name: "bitcoin_rpc".to_string(),
526 data: json!({
527 "method": "getrawtransaction",
528 "params": [txid, true]
529 }),
530 })
531 }
532 "get_mempool_info" => Ok(ClientActionResult::Custom {
533 name: "bitcoin_rpc".to_string(),
534 data: json!({
535 "method": "getmempoolinfo",
536 "params": []
537 }),
538 }),
539 "get_raw_mempool" => {
540 let verbose = action
541 .get("verbose")
542 .and_then(|v| v.as_bool())
543 .unwrap_or(false);
544
545 Ok(ClientActionResult::Custom {
546 name: "bitcoin_rpc".to_string(),
547 data: json!({
548 "method": "getrawmempool",
549 "params": [verbose]
550 }),
551 })
552 }
553 "get_mining_info" => Ok(ClientActionResult::Custom {
554 name: "bitcoin_rpc".to_string(),
555 data: json!({
556 "method": "getmininginfo",
557 "params": []
558 }),
559 }),
560 "get_network_info" => Ok(ClientActionResult::Custom {
561 name: "bitcoin_rpc".to_string(),
562 data: json!({
563 "method": "getnetworkinfo",
564 "params": []
565 }),
566 }),
567 "get_peer_info" => Ok(ClientActionResult::Custom {
568 name: "bitcoin_rpc".to_string(),
569 data: json!({
570 "method": "getpeerinfo",
571 "params": []
572 }),
573 }),
574 "get_connection_count" => Ok(ClientActionResult::Custom {
575 name: "bitcoin_rpc".to_string(),
576 data: json!({
577 "method": "getconnectioncount",
578 "params": []
579 }),
580 }),
581 "get_wallet_info" => Ok(ClientActionResult::Custom {
582 name: "bitcoin_rpc".to_string(),
583 data: json!({
584 "method": "getwalletinfo",
585 "params": []
586 }),
587 }),
588 "get_balance" => Ok(ClientActionResult::Custom {
589 name: "bitcoin_rpc".to_string(),
590 data: json!({
591 "method": "getbalance",
592 "params": []
593 }),
594 }),
595 "list_transactions" => {
596 let count = action.get("count").and_then(|v| v.as_u64()).unwrap_or(10);
597
598 Ok(ClientActionResult::Custom {
599 name: "bitcoin_rpc".to_string(),
600 data: json!({
601 "method": "listtransactions",
602 "params": ["*", count]
603 }),
604 })
605 }
606 "execute_rpc" => {
607 let method = action
608 .get("method")
609 .and_then(|v| v.as_str())
610 .context("Missing 'method' field")?
611 .to_string();
612
613 let params = action
614 .get("params")
615 .and_then(|v| v.as_array())
616 .cloned()
617 .unwrap_or_default();
618
619 Ok(ClientActionResult::Custom {
620 name: "bitcoin_rpc".to_string(),
621 data: json!({
622 "method": method,
623 "params": params
624 }),
625 })
626 }
627 "disconnect" => Ok(ClientActionResult::Disconnect),
628 _ => Err(anyhow::anyhow!(
629 "Unknown Bitcoin RPC client action: {}",
630 action_type
631 )),
632 }
633 }
634}