1use crate::error::{Error, Result};
4use crate::types::{Balance, Block, NetworkInfo};
5use crate::{
6 ContractInstruction, Hash, Instruction, Keypair, Pubkey, TransactionBuilder,
7 CONTRACT_PROGRAM_ID, SYSTEM_PROGRAM_ID,
8};
9use reqwest;
10use serde::{Deserialize, Serialize};
11use serde_json::{json, Value};
12use std::sync::atomic::{AtomicU64, Ordering};
13use std::sync::Arc;
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ReadonlyContractResult {
17 pub success: bool,
18 #[serde(rename = "returnData")]
19 pub return_data: Option<String>,
20 #[serde(rename = "returnCode")]
21 pub return_code: Option<u32>,
22 #[serde(default)]
23 pub logs: Vec<String>,
24 pub error: Option<String>,
25 #[serde(rename = "computeUsed")]
26 pub compute_used: Option<u64>,
27}
28
29#[derive(Debug, Clone)]
31pub struct Client {
32 rpc_url: String,
33 client: reqwest::Client,
34 next_id: Arc<AtomicU64>,
35}
36
37impl Client {
38 pub fn new(rpc_url: impl Into<String>) -> Self {
40 Self {
41 rpc_url: rpc_url.into(),
42 client: reqwest::Client::new(),
43 next_id: Arc::new(AtomicU64::new(1)),
44 }
45 }
46
47 pub fn from_env() -> Self {
49 let url =
50 std::env::var("LICHEN_RPC_URL").unwrap_or_else(|_| "http://localhost:8899".to_string());
51 Self::new(url)
52 }
53
54 pub fn builder() -> ClientBuilder {
56 ClientBuilder::default()
57 }
58
59 pub(crate) async fn rpc_call(&self, method: &str, params: Value) -> Result<Value> {
61 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
62 let request = json!({
63 "jsonrpc": "2.0",
64 "id": id,
65 "method": method,
66 "params": params
67 });
68
69 let response = self
70 .client
71 .post(&self.rpc_url)
72 .json(&request)
73 .send()
74 .await?
75 .json::<Value>()
76 .await?;
77
78 if let Some(error) = response.get("error") {
79 return Err(Error::RpcError(error.to_string()));
80 }
81
82 response
83 .get("result")
84 .cloned()
85 .ok_or(Error::RpcError("No result in response".to_string()))
86 }
87
88 pub async fn get_slot(&self) -> Result<u64> {
90 let result = self.rpc_call("getSlot", json!([])).await?;
91 result
92 .as_u64()
93 .ok_or(Error::ParseError("Invalid slot format".to_string()))
94 }
95
96 pub async fn get_balance(&self, pubkey: &Pubkey) -> Result<Balance> {
98 let result = self
99 .rpc_call("getBalance", json!([pubkey.to_base58()]))
100 .await?;
101
102 let spores = result["spores"]
103 .as_u64()
104 .ok_or(Error::ParseError("Invalid balance format".to_string()))?;
105
106 Ok(Balance::from_spores(spores))
107 }
108
109 pub async fn get_block(&self, slot: u64) -> Result<Block> {
111 let result = self.rpc_call("getBlock", json!([slot])).await?;
112 serde_json::from_value(result).map_err(|e| Error::ParseError(e.to_string()))
113 }
114
115 pub async fn get_latest_block(&self) -> Result<Block> {
117 let result = self.rpc_call("getLatestBlock", json!([])).await?;
118 serde_json::from_value(result).map_err(|e| Error::ParseError(e.to_string()))
119 }
120
121 pub async fn get_network_info(&self) -> Result<NetworkInfo> {
123 let result = self.rpc_call("getNetworkInfo", json!([])).await?;
124 serde_json::from_value(result).map_err(|e| Error::ParseError(e.to_string()))
125 }
126
127 pub async fn get_validators(&self) -> Result<Vec<Value>> {
129 let result = self.rpc_call("getValidators", json!([])).await?;
130 if let Some(arr) = result.as_array() {
132 Ok(arr.clone())
133 } else if let Some(validators) = result.get("validators").and_then(|v| v.as_array()) {
134 Ok(validators.clone())
135 } else {
136 Err(Error::ParseError("Invalid validators format".to_string()))
137 }
138 }
139
140 pub async fn send_raw_transaction(&self, tx_base64: &str) -> Result<String> {
142 let result = self.rpc_call("sendTransaction", json!([tx_base64])).await?;
143 result
144 .as_str()
145 .map(|s| s.to_string())
146 .ok_or(Error::ParseError("Invalid transaction hash".to_string()))
147 }
148
149 pub async fn send_transaction(&self, tx: &crate::types::Transaction) -> Result<String> {
151 let tx_bytes = tx.to_wire();
152 let tx_base64 =
153 base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &tx_bytes);
154 self.send_raw_transaction(&tx_base64).await
155 }
156
157 pub async fn get_transaction(&self, signature: &str) -> Result<Value> {
159 self.rpc_call("getTransaction", json!([signature])).await
160 }
161
162 pub async fn get_account_info(&self, pubkey: &Pubkey) -> Result<Value> {
164 self.rpc_call("getAccountInfo", json!([pubkey.to_base58()]))
165 .await
166 }
167
168 pub async fn get_transaction_history(
170 &self,
171 pubkey: &Pubkey,
172 limit: Option<u64>,
173 ) -> Result<Value> {
174 let limit = limit.unwrap_or(10);
175 self.rpc_call("getTransactionHistory", json!([pubkey.to_base58(), limit]))
176 .await
177 }
178
179 pub async fn call_readonly_contract(
181 &self,
182 contract: &Pubkey,
183 function: &str,
184 args: Vec<u8>,
185 from: Option<&Pubkey>,
186 ) -> Result<ReadonlyContractResult> {
187 let args_b64 = base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &args);
188 let mut params = vec![
189 json!(contract.to_base58()),
190 json!(function),
191 json!(args_b64),
192 ];
193 if let Some(from_pubkey) = from {
194 params.push(json!(from_pubkey.to_base58()));
195 }
196
197 let result = self.rpc_call("callContract", Value::Array(params)).await?;
198 serde_json::from_value(result).map_err(|err| Error::ParseError(err.to_string()))
199 }
200
201 pub async fn get_recent_blockhash(&self) -> Result<String> {
203 let result = self.rpc_call("getRecentBlockhash", json!([])).await?;
204 if let Some(hash_str) = result.as_str() {
206 Ok(hash_str.to_string())
207 } else if let Some(hash_str) = result.get("blockhash").and_then(|v| v.as_str()) {
208 Ok(hash_str.to_string())
209 } else {
210 Err(Error::ParseError("Invalid blockhash format".to_string()))
211 }
212 }
213
214 pub async fn get_validator_info(&self, pubkey: &Pubkey) -> Result<Value> {
220 self.rpc_call("getValidatorInfo", json!([pubkey.to_base58()]))
221 .await
222 }
223
224 pub async fn get_validator_performance(&self, pubkey: &Pubkey) -> Result<Value> {
226 self.rpc_call("getValidatorPerformance", json!([pubkey.to_base58()]))
227 .await
228 }
229
230 pub async fn get_chain_status(&self) -> Result<Value> {
232 self.rpc_call("getChainStatus", json!([])).await
233 }
234
235 pub async fn stake(&self, staker: &Keypair, validator: &Pubkey, amount: u64) -> Result<String> {
241 let blockhash_str = self.get_recent_blockhash().await?;
242 let blockhash = Hash::from_hex(&blockhash_str).map_err(|e| Error::ParseError(e))?;
243
244 let mut data = vec![9u8];
245 data.extend_from_slice(&amount.to_le_bytes());
246
247 let instruction = Instruction {
248 program_id: SYSTEM_PROGRAM_ID,
249 accounts: vec![staker.pubkey(), *validator],
250 data,
251 };
252
253 let tx = TransactionBuilder::new()
254 .add_instruction(instruction)
255 .recent_blockhash(blockhash)
256 .build_and_sign(staker)?;
257
258 self.send_transaction(&tx).await
259 }
260
261 pub async fn unstake(
263 &self,
264 staker: &Keypair,
265 validator: &Pubkey,
266 amount: u64,
267 ) -> Result<String> {
268 let blockhash_str = self.get_recent_blockhash().await?;
269 let blockhash = Hash::from_hex(&blockhash_str).map_err(|e| Error::ParseError(e))?;
270
271 let mut data = vec![10u8];
272 data.extend_from_slice(&amount.to_le_bytes());
273
274 let instruction = Instruction {
275 program_id: SYSTEM_PROGRAM_ID,
276 accounts: vec![staker.pubkey(), *validator],
277 data,
278 };
279
280 let tx = TransactionBuilder::new()
281 .add_instruction(instruction)
282 .recent_blockhash(blockhash)
283 .build_and_sign(staker)?;
284
285 self.send_transaction(&tx).await
286 }
287
288 pub async fn get_staking_status(&self, pubkey: &Pubkey) -> Result<Value> {
290 self.rpc_call("getStakingStatus", json!([pubkey.to_base58()]))
291 .await
292 }
293
294 pub async fn get_staking_rewards(&self, pubkey: &Pubkey) -> Result<Value> {
296 self.rpc_call("getStakingRewards", json!([pubkey.to_base58()]))
297 .await
298 }
299
300 pub async fn transfer(&self, from: &Keypair, to: &Pubkey, amount: u64) -> Result<String> {
306 let blockhash_str = self.get_recent_blockhash().await?;
307 let blockhash = Hash::from_hex(&blockhash_str).map_err(|e| Error::ParseError(e))?;
308
309 let mut data = vec![0u8]; data.extend_from_slice(&amount.to_le_bytes());
311
312 let instruction = Instruction {
313 program_id: SYSTEM_PROGRAM_ID,
314 accounts: vec![from.pubkey(), *to],
315 data,
316 };
317
318 let tx = TransactionBuilder::new()
319 .add_instruction(instruction)
320 .recent_blockhash(blockhash)
321 .build_and_sign(from)?;
322
323 self.send_transaction(&tx).await
324 }
325
326 pub async fn deploy_contract(
333 &self,
334 deployer: &Keypair,
335 code: Vec<u8>,
336 init_data: Vec<u8>,
337 ) -> Result<String> {
338 if code.len() < 4 || &code[..4] != b"\0asm" {
339 return Err(Error::BuildError(
340 "Invalid WASM bytecode: missing magic header (\\0asm)".into(),
341 ));
342 }
343 if code.len() > 512 * 1024 {
344 return Err(Error::BuildError(
345 "Contract code exceeds 512 KB limit".into(),
346 ));
347 }
348
349 let blockhash_str = self.get_recent_blockhash().await?;
350 let blockhash = Hash::from_hex(&blockhash_str).map_err(|e| Error::ParseError(e))?;
351
352 let contract_ix = ContractInstruction::Deploy { code, init_data };
353 let data = serde_json::to_vec(&contract_ix)
354 .map_err(|e| Error::SerializationError(e.to_string()))?;
355
356 let instruction = Instruction {
357 program_id: CONTRACT_PROGRAM_ID,
358 accounts: vec![deployer.pubkey()],
359 data,
360 };
361
362 let tx = TransactionBuilder::new()
363 .add_instruction(instruction)
364 .recent_blockhash(blockhash)
365 .build_and_sign(deployer)?;
366
367 self.send_transaction(&tx).await
368 }
369
370 pub async fn call_contract(
379 &self,
380 caller: &Keypair,
381 contract: &Pubkey,
382 function: &str,
383 args: Vec<u8>,
384 value: u64,
385 ) -> Result<String> {
386 let blockhash_str = self.get_recent_blockhash().await?;
387 let blockhash = Hash::from_hex(&blockhash_str).map_err(|e| Error::ParseError(e))?;
388
389 let contract_ix = ContractInstruction::Call {
390 function: function.to_string(),
391 args,
392 value,
393 };
394 let data = serde_json::to_vec(&contract_ix)
395 .map_err(|e| Error::SerializationError(e.to_string()))?;
396
397 let instruction = Instruction {
398 program_id: CONTRACT_PROGRAM_ID,
399 accounts: vec![caller.pubkey(), *contract],
400 data,
401 };
402
403 let tx = TransactionBuilder::new()
404 .add_instruction(instruction)
405 .recent_blockhash(blockhash)
406 .build_and_sign(caller)?;
407
408 self.send_transaction(&tx).await
409 }
410
411 pub async fn upgrade_contract(
413 &self,
414 owner: &Keypair,
415 contract: &Pubkey,
416 code: Vec<u8>,
417 ) -> Result<String> {
418 if code.len() < 4 || &code[..4] != b"\0asm" {
419 return Err(Error::BuildError(
420 "Invalid WASM bytecode: missing magic header (\\0asm)".into(),
421 ));
422 }
423 if code.len() > 512 * 1024 {
424 return Err(Error::BuildError(
425 "Contract code exceeds 512 KB limit".into(),
426 ));
427 }
428
429 let blockhash_str = self.get_recent_blockhash().await?;
430 let blockhash = Hash::from_hex(&blockhash_str).map_err(|e| Error::ParseError(e))?;
431
432 let contract_ix = ContractInstruction::Upgrade { code };
433 let data = serde_json::to_vec(&contract_ix)
434 .map_err(|e| Error::SerializationError(e.to_string()))?;
435
436 let instruction = Instruction {
437 program_id: CONTRACT_PROGRAM_ID,
438 accounts: vec![owner.pubkey(), *contract],
439 data,
440 };
441
442 let tx = TransactionBuilder::new()
443 .add_instruction(instruction)
444 .recent_blockhash(blockhash)
445 .build_and_sign(owner)?;
446
447 self.send_transaction(&tx).await
448 }
449
450 pub async fn get_peers(&self) -> Result<Value> {
456 self.rpc_call("getPeers", json!([])).await
457 }
458
459 pub async fn get_metrics(&self) -> Result<Value> {
461 self.rpc_call("getMetrics", json!([])).await
462 }
463
464 pub async fn get_total_burned(&self) -> Result<Value> {
466 self.rpc_call("getTotalBurned", json!([])).await
467 }
468
469 pub async fn get_contract_info(&self, contract_id: &Pubkey) -> Result<Value> {
475 self.rpc_call("getContractInfo", json!([contract_id.to_base58()]))
476 .await
477 }
478
479 pub async fn get_contract_logs(&self, contract_id: &Pubkey) -> Result<Value> {
481 self.rpc_call("getContractLogs", json!([contract_id.to_base58()]))
482 .await
483 }
484
485 pub async fn get_symbol_registry(&self, symbol: &str) -> Result<Value> {
487 self.rpc_call("getSymbolRegistry", json!([symbol])).await
488 }
489
490 pub async fn get_lichenid_profile(&self, pubkey: &Pubkey) -> Result<Value> {
492 self.rpc_call("getLichenIdProfile", json!([pubkey.to_base58()]))
493 .await
494 }
495
496 pub async fn get_lichenid_reputation(&self, pubkey: &Pubkey) -> Result<Value> {
498 self.rpc_call("getLichenIdReputation", json!([pubkey.to_base58()]))
499 .await
500 }
501
502 pub async fn get_lichenid_skills(&self, pubkey: &Pubkey) -> Result<Value> {
504 self.rpc_call("getLichenIdSkills", json!([pubkey.to_base58()]))
505 .await
506 }
507
508 pub async fn get_lichenid_vouches(&self, pubkey: &Pubkey) -> Result<Value> {
510 self.rpc_call("getLichenIdVouches", json!([pubkey.to_base58()]))
511 .await
512 }
513
514 pub async fn resolve_lichen_name(&self, name: &str) -> Result<Value> {
516 self.rpc_call("resolveLichenName", json!([name])).await
517 }
518
519 pub async fn get_name_auction(&self, name: &str) -> Result<Value> {
521 self.rpc_call("getNameAuction", json!([name])).await
522 }
523
524 pub async fn get_lichenid_agent_directory(&self, options: Option<Value>) -> Result<Value> {
526 match options {
527 Some(options) => {
528 self.rpc_call("getLichenIdAgentDirectory", json!([options]))
529 .await
530 }
531 None => self.rpc_call("getLichenIdAgentDirectory", json!([])).await,
532 }
533 }
534
535 pub async fn get_lichenid_stats(&self) -> Result<Value> {
537 self.rpc_call("getLichenIdStats", json!([])).await
538 }
539
540 pub async fn get_sporepay_stats(&self) -> Result<Value> {
542 self.rpc_call("getSporePayStats", json!([])).await
543 }
544
545 pub async fn get_lichenswap_stats(&self) -> Result<Value> {
547 self.rpc_call("getLichenSwapStats", json!([])).await
548 }
549
550 pub async fn get_thalllend_stats(&self) -> Result<Value> {
552 self.rpc_call("getThallLendStats", json!([])).await
553 }
554
555 pub async fn get_sporevault_stats(&self) -> Result<Value> {
557 self.rpc_call("getSporeVaultStats", json!([])).await
558 }
559
560 pub async fn get_neo_gas_rewards_stats(&self) -> Result<Value> {
562 self.rpc_call("getNeoGasRewardsStats", json!([])).await
563 }
564
565 pub async fn get_neo_gas_rewards_position(&self, address: &Pubkey) -> Result<Value> {
567 self.rpc_call("getNeoGasRewardsPosition", json!([address.to_base58()]))
568 .await
569 }
570
571 pub async fn get_neo_zk_proof_service_status(&self) -> Result<Value> {
573 self.rpc_call("getNeoZkProofServiceStatus", json!([])).await
574 }
575
576 pub async fn verify_neo_reserve_liability_proof(&self, proof_envelope: Value) -> Result<Value> {
578 self.rpc_call("verifyNeoReserveLiabilityProof", json!([proof_envelope]))
579 .await
580 }
581
582 pub async fn get_bountyboard_stats(&self) -> Result<Value> {
584 self.rpc_call("getBountyBoardStats", json!([])).await
585 }
586
587 pub async fn get_program(&self, program_id: &Pubkey) -> Result<Value> {
592 self.rpc_call("getProgram", json!([program_id.to_base58()]))
593 .await
594 }
595
596 pub async fn get_program_stats(&self, program_id: &Pubkey) -> Result<Value> {
597 self.rpc_call("getProgramStats", json!([program_id.to_base58()]))
598 .await
599 }
600
601 pub async fn get_programs(&self) -> Result<Value> {
602 self.rpc_call("getPrograms", json!([])).await
603 }
604
605 pub async fn get_program_calls(&self, program_id: &Pubkey) -> Result<Value> {
606 self.rpc_call("getProgramCalls", json!([program_id.to_base58()]))
607 .await
608 }
609
610 pub async fn get_program_storage(&self, program_id: &Pubkey) -> Result<Value> {
611 self.rpc_call("getProgramStorage", json!([program_id.to_base58()]))
612 .await
613 }
614
615 pub async fn get_collection(&self, collection_id: &Pubkey) -> Result<Value> {
620 self.rpc_call("getCollection", json!([collection_id.to_base58()]))
621 .await
622 }
623
624 pub async fn get_nft(&self, collection_id: &Pubkey, token_id: u64) -> Result<Value> {
625 self.rpc_call("getNFT", json!([collection_id.to_base58(), token_id]))
626 .await
627 }
628
629 pub async fn get_nfts_by_owner(&self, owner: &Pubkey) -> Result<Value> {
630 self.rpc_call("getNFTsByOwner", json!([owner.to_base58()]))
631 .await
632 }
633
634 pub async fn get_nfts_by_collection(&self, collection_id: &Pubkey) -> Result<Value> {
635 self.rpc_call("getNFTsByCollection", json!([collection_id.to_base58()]))
636 .await
637 }
638
639 pub async fn get_nft_activity(&self, collection_id: &Pubkey, token_id: u64) -> Result<Value> {
640 self.rpc_call(
641 "getNFTActivity",
642 json!([collection_id.to_base58(), token_id]),
643 )
644 .await
645 }
646
647 pub async fn get_all_contracts(&self) -> Result<Value> {
649 self.rpc_call("getAllContracts", json!([])).await
650 }
651
652 pub async fn health(&self) -> Result<bool> {
654 let result = self.rpc_call("health", json!([])).await?;
655 Ok(result.get("status").and_then(|v| v.as_str()) == Some("ok"))
656 }
657}
658
659#[derive(Default)]
661pub struct ClientBuilder {
662 rpc_url: Option<String>,
663 timeout: Option<std::time::Duration>,
664}
665
666impl ClientBuilder {
667 pub fn rpc_url(mut self, url: impl Into<String>) -> Self {
669 self.rpc_url = Some(url.into());
670 self
671 }
672
673 pub fn timeout(mut self, timeout: std::time::Duration) -> Self {
675 self.timeout = Some(timeout);
676 self
677 }
678
679 pub fn build(self) -> Result<Client> {
681 let rpc_url = self
682 .rpc_url
683 .ok_or(Error::ConfigError("RPC URL not set".to_string()))?;
684
685 let mut client_builder = reqwest::Client::builder();
686
687 if let Some(timeout) = self.timeout {
688 client_builder = client_builder.timeout(timeout);
689 }
690
691 Ok(Client {
692 rpc_url,
693 client: client_builder.build()?,
694 next_id: Arc::new(AtomicU64::new(1)),
695 })
696 }
697}
698
699#[cfg(test)]
700mod tests {
701 use super::*;
702 use std::sync::{Mutex, OnceLock};
703
704 fn env_lock() -> &'static Mutex<()> {
705 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
706 LOCK.get_or_init(|| Mutex::new(()))
707 }
708
709 #[test]
712 fn test_client_new() {
713 let client = Client::new("http://localhost:8899");
714 assert_eq!(client.rpc_url, "http://localhost:8899");
715 }
716
717 #[test]
718 fn test_client_new_custom_url() {
719 let client = Client::new("https://rpc.lichen.network:443");
720 assert_eq!(client.rpc_url, "https://rpc.lichen.network:443");
721 }
722
723 #[test]
724 fn test_client_new_id_starts_at_1() {
725 let client = Client::new("http://localhost:8899");
726 assert_eq!(client.next_id.load(Ordering::Relaxed), 1);
727 }
728
729 #[test]
732 fn test_client_from_env_defaults_to_localhost() {
733 let _guard = env_lock().lock().unwrap();
734 std::env::remove_var("LICHEN_RPC_URL");
736 let client = Client::from_env();
737 assert_eq!(client.rpc_url, "http://localhost:8899");
738 }
739
740 #[test]
741 fn test_client_from_env_uses_var() {
742 let _guard = env_lock().lock().unwrap();
743 std::env::set_var("LICHEN_RPC_URL", "http://custom:9999");
744 let client = Client::from_env();
745 assert_eq!(client.rpc_url, "http://custom:9999");
746 std::env::remove_var("LICHEN_RPC_URL");
747 }
748
749 #[test]
752 fn test_client_builder() {
753 let client = Client::builder()
754 .rpc_url("http://localhost:8899")
755 .timeout(std::time::Duration::from_secs(30))
756 .build()
757 .expect("should build client");
758 assert_eq!(client.rpc_url, "http://localhost:8899");
759 }
760
761 #[test]
762 fn test_client_builder_no_url_fails() {
763 let result = Client::builder().build();
764 assert!(result.is_err(), "should fail without URL");
765 }
766
767 #[test]
768 fn test_client_builder_no_timeout() {
769 let client = Client::builder()
770 .rpc_url("http://localhost:8899")
771 .build()
772 .expect("should build without timeout");
773 assert_eq!(client.rpc_url, "http://localhost:8899");
774 }
775
776 #[test]
777 fn test_client_builder_default() {
778 let builder = ClientBuilder::default();
779 assert!(builder.rpc_url.is_none());
780 assert!(builder.timeout.is_none());
781 }
782
783 #[test]
786 fn test_client_id_increments() {
787 let client = Client::new("http://localhost:8899");
788 let v1 = client.next_id.fetch_add(1, Ordering::Relaxed);
789 let v2 = client.next_id.fetch_add(1, Ordering::Relaxed);
790 assert_eq!(v1, 1);
791 assert_eq!(v2, 2);
792 }
793
794 #[test]
795 fn test_client_clone_shares_counter() {
796 let client = Client::new("http://localhost:8899");
797 client.next_id.fetch_add(1, Ordering::Relaxed);
798 let clone = client.clone();
799 let v = clone.next_id.fetch_add(1, Ordering::Relaxed);
800 assert_eq!(v, 2); }
802
803 #[test]
804 fn test_client_clone_shares_url() {
805 let client = Client::new("http://my-rpc:8899");
806 let clone = client.clone();
807 assert_eq!(clone.rpc_url, "http://my-rpc:8899");
808 }
809}