|
1 | | -use std::sync::{Arc, Mutex}; |
2 | | -use std::thread; |
3 | | -use std::time::Duration; |
4 | | -use serde_json::json; |
5 | | -use mcp_sdk::transport::{Transport, JsonRpcMessage, JsonRpcRequest, JsonRpcResponse, JsonRpcVersion, JsonRpcError}; |
6 | | -use std::sync::mpsc::{self, Sender, Receiver}; |
7 | | -use anyhow::Result; |
| 1 | +use solana_client::nonblocking::rpc_client::RpcClient; |
| 2 | +use solana_sdk::{ |
| 3 | + commitment_config::CommitmentConfig, |
| 4 | + pubkey::Pubkey, |
| 5 | +}; |
8 | 6 |
|
9 | | -struct TestTransport { |
10 | | - tx: Sender<serde_json::Value>, |
11 | | - rx: Arc<Mutex<Receiver<serde_json::Value>>>, |
12 | | -} |
13 | | - |
14 | | -impl TestTransport { |
15 | | - fn new() -> (Self, Self) { |
16 | | - let (client_tx, server_rx) = mpsc::channel(); |
17 | | - let (server_tx, client_rx) = mpsc::channel(); |
18 | | - |
19 | | - let client = Self { |
20 | | - tx: client_tx, |
21 | | - rx: Arc::new(Mutex::new(client_rx)), |
22 | | - }; |
23 | | - |
24 | | - let server = Self { |
25 | | - tx: server_tx, |
26 | | - rx: Arc::new(Mutex::new(server_rx)), |
27 | | - }; |
28 | | - |
29 | | - (client, server) |
30 | | - } |
31 | | -} |
32 | | - |
33 | | -impl Transport for TestTransport { |
34 | | - fn send(&self, message: &JsonRpcMessage) -> Result<()> { |
35 | | - let json = match message { |
36 | | - JsonRpcMessage::Request(req) => { |
37 | | - json!({ |
38 | | - "jsonrpc": JsonRpcVersion::V2, |
39 | | - "id": req.id, |
40 | | - "method": req.method, |
41 | | - "params": req.params |
42 | | - }) |
43 | | - }, |
44 | | - JsonRpcMessage::Response(resp) => { |
45 | | - json!({ |
46 | | - "jsonrpc": JsonRpcVersion::V2, |
47 | | - "id": resp.id, |
48 | | - "result": resp.result, |
49 | | - "error": resp.error |
50 | | - }) |
51 | | - }, |
52 | | - JsonRpcMessage::Notification(_) => { |
53 | | - json!({}) |
54 | | - } |
55 | | - }; |
56 | | - |
57 | | - self.tx.send(json).unwrap(); |
58 | | - Ok(()) |
59 | | - } |
60 | | - |
61 | | - fn receive(&self) -> Result<JsonRpcMessage> { |
62 | | - let value = match self.rx.lock().unwrap().recv_timeout(Duration::from_secs(5)) { |
63 | | - Ok(value) => value, |
64 | | - Err(_) => panic!("Failed to receive message within timeout"), |
65 | | - }; |
66 | | - |
67 | | - if value.get("error").is_some() { |
68 | | - Ok(JsonRpcMessage::Response(JsonRpcResponse { |
69 | | - jsonrpc: JsonRpcVersion::default(), |
70 | | - id: value["id"].as_u64().unwrap(), |
71 | | - result: None, |
72 | | - error: Some(JsonRpcError { |
73 | | - code: value["error"]["code"].as_i64().unwrap() as i32, |
74 | | - message: value["error"]["message"].as_str().unwrap().to_string(), |
75 | | - data: value["error"].get("data").cloned(), |
76 | | - }), |
77 | | - })) |
78 | | - } else if value.get("result").is_some() { |
79 | | - Ok(JsonRpcMessage::Response(JsonRpcResponse { |
80 | | - jsonrpc: JsonRpcVersion::default(), |
81 | | - id: value["id"].as_u64().unwrap(), |
82 | | - result: Some(value["result"].clone()), |
83 | | - error: None, |
84 | | - })) |
85 | | - } else { |
86 | | - Ok(JsonRpcMessage::Request(JsonRpcRequest { |
87 | | - jsonrpc: JsonRpcVersion::default(), |
88 | | - id: value["id"].as_u64().unwrap(), |
89 | | - method: value["method"].as_str().unwrap().to_string(), |
90 | | - params: value.get("params").cloned(), |
91 | | - })) |
92 | | - } |
93 | | - } |
94 | | - |
95 | | - fn open(&self) -> Result<()> { |
96 | | - Ok(()) |
97 | | - } |
98 | | - |
99 | | - fn close(&self) -> Result<()> { |
100 | | - Ok(()) |
101 | | - } |
102 | | -} |
103 | | - |
104 | | -fn setup_mock_server() -> TestTransport { |
105 | | - let (client_transport, server_transport) = TestTransport::new(); |
| 7 | +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
| 8 | +async fn test_blockchain_operations() { |
| 9 | + // Connect to Solana devnet |
| 10 | + let rpc_url = "https://api.opensvm.com".to_string(); |
| 11 | + let client = RpcClient::new_with_commitment(rpc_url.clone(), CommitmentConfig::confirmed()); |
106 | 12 |
|
107 | | - thread::spawn(move || { |
108 | | - loop { |
109 | | - if let Ok(msg) = server_transport.rx.lock().unwrap().recv() { |
110 | | - let id = msg["id"].as_u64().unwrap(); |
111 | | - let method = msg["method"].as_str().unwrap(); |
112 | | - let auth = msg["params"]["auth"].as_str().unwrap(); |
113 | | - |
114 | | - match (method, auth) { |
115 | | - ("initialize", "test_key_123") => { |
116 | | - server_transport.tx.send(json!({ |
117 | | - "jsonrpc": JsonRpcVersion::V2, |
118 | | - "id": id, |
119 | | - "result": { |
120 | | - "server": { |
121 | | - "name": "solana-mcp", |
122 | | - "version": "0.1.2" |
123 | | - }, |
124 | | - "protocol": { |
125 | | - "name": "mcp", |
126 | | - "version": "0.1.0" |
127 | | - } |
128 | | - } |
129 | | - })).unwrap(); |
130 | | - }, |
131 | | - (_, "invalid_key") => { |
132 | | - server_transport.tx.send(json!({ |
133 | | - "jsonrpc": JsonRpcVersion::V2, |
134 | | - "id": id, |
135 | | - "error": { |
136 | | - "code": -32601, |
137 | | - "message": "Invalid API key" |
138 | | - } |
139 | | - })).unwrap(); |
140 | | - }, |
141 | | - ("tools/call", "test_key_123") => { |
142 | | - let tool_name = msg["params"]["name"].as_str().unwrap(); |
143 | | - match tool_name { |
144 | | - "get_slot" => { |
145 | | - server_transport.tx.send(json!({ |
146 | | - "jsonrpc": JsonRpcVersion::V2, |
147 | | - "id": id, |
148 | | - "result": 12345 |
149 | | - })).unwrap(); |
150 | | - }, |
151 | | - _ => { |
152 | | - server_transport.tx.send(json!({ |
153 | | - "jsonrpc": JsonRpcVersion::V2, |
154 | | - "id": id, |
155 | | - "error": { |
156 | | - "code": -32601, |
157 | | - "message": "Tool not found" |
158 | | - } |
159 | | - })).unwrap(); |
160 | | - } |
161 | | - } |
162 | | - }, |
163 | | - _ => {} |
164 | | - } |
165 | | - } |
| 13 | + // Configure RPC client to support latest transaction versions |
| 14 | + let config = solana_client::rpc_config::RpcBlockConfig { |
| 15 | + encoding: None, |
| 16 | + transaction_details: None, |
| 17 | + rewards: None, |
| 18 | + commitment: None, |
| 19 | + max_supported_transaction_version: Some(0), |
| 20 | + }; |
| 21 | + |
| 22 | + println!("\nTesting health check:"); |
| 23 | + let health = client.get_health().await.unwrap(); |
| 24 | + println!("Health status: {:?}", health); |
| 25 | + |
| 26 | + println!("\nTesting version info:"); |
| 27 | + let version = client.get_version().await.unwrap(); |
| 28 | + println!("Version info: {:?}", version); |
| 29 | + |
| 30 | + println!("\nTesting latest blockhash:"); |
| 31 | + let blockhash = client.get_latest_blockhash().await.unwrap(); |
| 32 | + println!("Latest blockhash: {:?}", blockhash); |
| 33 | + |
| 34 | + println!("\nTesting transaction count:"); |
| 35 | + let count = client.get_transaction_count().await.unwrap(); |
| 36 | + println!("Transaction count: {}", count); |
| 37 | + |
| 38 | + // Get info about the System Program |
| 39 | + println!("\nTesting account info for System Program:"); |
| 40 | + let system_program_id = "11111111111111111111111111111111".parse::<Pubkey>().unwrap(); |
| 41 | + let account = client.get_account(&system_program_id).await.unwrap(); |
| 42 | + println!("System Program Account:"); |
| 43 | + println!(" Owner: {}", account.owner); |
| 44 | + println!(" Lamports: {}", account.lamports); |
| 45 | + println!(" Executable: {}", account.executable); |
| 46 | + |
| 47 | + // Get recent confirmed signatures first |
| 48 | + println!("\nTesting recent transactions:"); |
| 49 | + let signatures = client.get_signatures_for_address(&system_program_id).await.unwrap(); |
| 50 | + println!("Recent transactions for System Program:"); |
| 51 | + for sig in signatures.iter().take(3) { |
| 52 | + println!(" Signature: {}", sig.signature); |
| 53 | + println!(" Slot: {}", sig.slot); |
| 54 | + if let Some(err) = &sig.err { |
| 55 | + println!(" Error: {:?}", err); |
166 | 56 | } |
167 | | - }); |
168 | | - |
169 | | - client_transport |
170 | | -} |
171 | | - |
172 | | -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
173 | | -async fn test_server_initialization() { |
174 | | - let transport = setup_mock_server(); |
175 | | - |
176 | | - let request = JsonRpcMessage::Request(JsonRpcRequest { |
177 | | - jsonrpc: JsonRpcVersion::default(), |
178 | | - id: 1, |
179 | | - method: "initialize".to_string(), |
180 | | - params: Some(json!({ |
181 | | - "auth": "test_key_123" |
182 | | - })), |
183 | | - }); |
184 | | - |
185 | | - transport.send(&request).unwrap(); |
186 | | - let response = transport.receive().unwrap(); |
187 | | - |
188 | | - match response { |
189 | | - JsonRpcMessage::Response(resp) => { |
190 | | - assert_eq!(resp.jsonrpc, JsonRpcVersion::V2); |
191 | | - assert_eq!(resp.id, 1); |
192 | | - let result = resp.result.unwrap(); |
193 | | - assert!(result["server"]["name"].as_str().unwrap().contains("solana-mcp")); |
194 | | - assert_eq!(result["protocol"]["name"].as_str().unwrap(), "mcp"); |
195 | | - }, |
196 | | - _ => panic!("Expected response message"), |
197 | 57 | } |
198 | | -} |
199 | | - |
200 | | -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
201 | | -async fn test_invalid_api_key() { |
202 | | - let transport = setup_mock_server(); |
203 | | - |
204 | | - let request = JsonRpcMessage::Request(JsonRpcRequest { |
205 | | - jsonrpc: JsonRpcVersion::default(), |
206 | | - id: 1, |
207 | | - method: "tools/list".to_string(), |
208 | | - params: Some(json!({ |
209 | | - "auth": "invalid_key" |
210 | | - })), |
211 | | - }); |
212 | | - |
213 | | - transport.send(&request).unwrap(); |
214 | | - let response = transport.receive().unwrap(); |
215 | | - |
216 | | - match response { |
217 | | - JsonRpcMessage::Response(resp) => { |
218 | | - assert_eq!(resp.jsonrpc, JsonRpcVersion::V2); |
219 | | - assert_eq!(resp.id, 1); |
220 | | - let error = resp.error.unwrap(); |
221 | | - assert_eq!(error.code, -32601); |
222 | | - assert!(error.message.contains("Invalid API key")); |
223 | | - }, |
224 | | - _ => panic!("Expected error message"), |
225 | | - } |
226 | | -} |
227 | | - |
228 | | -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
229 | | -async fn test_tool_execution() { |
230 | | - let transport = setup_mock_server(); |
231 | | - |
232 | | - // First initialize |
233 | | - let init_request = JsonRpcMessage::Request(JsonRpcRequest { |
234 | | - jsonrpc: JsonRpcVersion::default(), |
235 | | - id: 1, |
236 | | - method: "initialize".to_string(), |
237 | | - params: Some(json!({ |
238 | | - "auth": "test_key_123" |
239 | | - })), |
240 | | - }); |
241 | | - |
242 | | - transport.send(&init_request).unwrap(); |
243 | | - transport.receive().unwrap(); |
244 | 58 |
|
245 | | - // Then call tool |
246 | | - let request = JsonRpcMessage::Request(JsonRpcRequest { |
247 | | - jsonrpc: JsonRpcVersion::default(), |
248 | | - id: 2, |
249 | | - method: "tools/call".to_string(), |
250 | | - params: Some(json!({ |
251 | | - "auth": "test_key_123", |
252 | | - "name": "get_slot", |
253 | | - "arguments": {} |
254 | | - })), |
255 | | - }); |
256 | | - |
257 | | - transport.send(&request).unwrap(); |
258 | | - let response = transport.receive().unwrap(); |
259 | | - |
260 | | - match response { |
261 | | - JsonRpcMessage::Response(resp) => { |
262 | | - assert_eq!(resp.jsonrpc, JsonRpcVersion::V2); |
263 | | - assert_eq!(resp.id, 2); |
264 | | - assert!(resp.result.is_some()); |
265 | | - }, |
266 | | - _ => panic!("Expected response message"), |
267 | | - } |
268 | | -} |
269 | | - |
270 | | -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] |
271 | | -async fn test_invalid_tool() { |
272 | | - let transport = setup_mock_server(); |
273 | | - |
274 | | - // First initialize |
275 | | - let init_request = JsonRpcMessage::Request(JsonRpcRequest { |
276 | | - jsonrpc: JsonRpcVersion::default(), |
277 | | - id: 1, |
278 | | - method: "initialize".to_string(), |
279 | | - params: Some(json!({ |
280 | | - "auth": "test_key_123" |
281 | | - })), |
282 | | - }); |
283 | | - |
284 | | - transport.send(&init_request).unwrap(); |
285 | | - transport.receive().unwrap(); |
286 | | - |
287 | | - // Then call invalid tool |
288 | | - let request = JsonRpcMessage::Request(JsonRpcRequest { |
289 | | - jsonrpc: JsonRpcVersion::default(), |
290 | | - id: 2, |
291 | | - method: "tools/call".to_string(), |
292 | | - params: Some(json!({ |
293 | | - "auth": "test_key_123", |
294 | | - "name": "nonexistent_tool", |
295 | | - "arguments": {} |
296 | | - })), |
297 | | - }); |
298 | | - |
299 | | - transport.send(&request).unwrap(); |
300 | | - let response = transport.receive().unwrap(); |
301 | | - |
302 | | - match response { |
303 | | - JsonRpcMessage::Response(resp) => { |
304 | | - assert_eq!(resp.jsonrpc, JsonRpcVersion::V2); |
305 | | - assert_eq!(resp.id, 2); |
306 | | - let error = resp.error.unwrap(); |
307 | | - assert_eq!(error.code, -32601); |
308 | | - assert!(error.message.contains("Tool not found")); |
309 | | - }, |
310 | | - _ => panic!("Expected error message"), |
| 59 | + // Get block data using a slot we know exists from recent transactions |
| 60 | + if let Some(first_sig) = signatures.first() { |
| 61 | + println!("\nTesting block data for slot {}:", first_sig.slot); |
| 62 | + let block = client.get_block_with_config(first_sig.slot, config).await; |
| 63 | + match block { |
| 64 | + Ok(block) => println!("Block data: {:#?}", block), |
| 65 | + Err(e) => println!("Could not fetch block: {}", e), |
| 66 | + } |
311 | 67 | } |
312 | 68 | } |
0 commit comments