1#![allow(missing_docs)]
4#![allow(clippy::unwrap_used, clippy::expect_used, clippy::unused_async)]
5
6use crate::json_rpc_client::{canonical_json, JsonRpcClient, JsonRpcError};
7use crate::types::*;
8use crate::codec::{TransactionCodec, TransactionEnvelope as CodecTransactionEnvelope, TransactionSignature};
9use crate::AccOptions;
10use anyhow::Result;
11use ed25519_dalek::{SigningKey, Signer};
12use reqwest::Client;
13use serde_json::{json, Value};
14use sha2::{Digest, Sha256};
15use std::time::{SystemTime, UNIX_EPOCH};
16use url::Url;
17
18#[derive(Debug, Clone)]
20pub struct AccumulateClient {
21 pub v2_client: JsonRpcClient,
22 pub v3_client: JsonRpcClient,
23 pub options: AccOptions,
24}
25
26impl AccumulateClient {
27 pub async fn new_with_options(
29 v2_url: Url,
30 v3_url: Url,
31 options: AccOptions,
32 ) -> Result<Self, JsonRpcError> {
33 let mut client_builder = Client::builder().timeout(options.timeout);
34
35 if !options.headers.is_empty() {
37 let mut headers = reqwest::header::HeaderMap::new();
38 for (key, value) in &options.headers {
39 let header_name =
40 reqwest::header::HeaderName::from_bytes(key.as_bytes()).map_err(|e| {
41 JsonRpcError::General(anyhow::anyhow!("Invalid header name: {}", e))
42 })?;
43 let header_value = reqwest::header::HeaderValue::from_str(value).map_err(|e| {
44 JsonRpcError::General(anyhow::anyhow!("Invalid header value: {}", e))
45 })?;
46 headers.insert(header_name, header_value);
47 }
48 client_builder = client_builder.default_headers(headers);
49 }
50
51 let http_client = client_builder.build()?;
52
53 let v2_client = JsonRpcClient::with_client(v2_url, http_client.clone())?;
54 let v3_client = JsonRpcClient::with_client(v3_url, http_client)?;
55
56 Ok(Self {
57 v2_client,
58 v3_client,
59 options,
60 })
61 }
62
63 pub async fn status(&self) -> Result<StatusResponse, JsonRpcError> {
67 self.v2_client.call_v2("status", None).await
68 }
69
70 pub async fn query_tx(&self, hash: &str) -> Result<TransactionResponse, JsonRpcError> {
72 self.v2_client.call_v2(&format!("tx/{}", hash), None).await
73 }
74
75 pub async fn query_account(&self, url: &str) -> Result<Account, JsonRpcError> {
77 self.v2_client.call_v2(&format!("acc/{}", url), None).await
78 }
79
80 pub async fn faucet(&self, account_url: &str) -> Result<FaucetResponse, JsonRpcError> {
82 let payload = json!({
83 "account": account_url
84 });
85 self.v2_client.call_v2("faucet", Some(payload)).await
86 }
87
88 pub async fn submit_v2(&self, tx: &Value) -> Result<TransactionResponse, JsonRpcError> {
90 self.v2_client.call_v2("tx", Some(tx.clone())).await
91 }
92
93 pub async fn submit(
97 &self,
98 envelope: &TransactionEnvelope,
99 ) -> Result<V3SubmitResponse, JsonRpcError> {
100 let request = V3SubmitRequest {
101 envelope: envelope.clone(),
102 };
103 self.v3_client.call_v3("submit", json!(request)).await
104 }
105
106 pub async fn submit_multi(
108 &self,
109 envelopes: &[TransactionEnvelope],
110 ) -> Result<Vec<V3SubmitResponse>, JsonRpcError> {
111 let requests: Vec<V3SubmitRequest> = envelopes
112 .iter()
113 .map(|env| V3SubmitRequest {
114 envelope: env.clone(),
115 })
116 .collect();
117 self.v3_client.call_v3("submitMulti", json!(requests)).await
118 }
119
120 pub async fn query(&self, url: &str) -> Result<QueryResponse<Account>, JsonRpcError> {
122 let params = json!({ "url": url });
123 self.v3_client.call_v3("query", params).await
124 }
125
126 pub async fn query_block(&self, height: i64) -> Result<QueryResponse<Value>, JsonRpcError> {
128 let params = json!({ "height": height });
129 self.v3_client.call_v3("queryBlock", params).await
130 }
131
132 pub async fn node_info(
138 &self,
139 opts: crate::types::NodeInfoOptions,
140 ) -> Result<crate::types::V3NodeInfo, JsonRpcError> {
141 self.v3_client.call_v3("node-info", json!(opts)).await
142 }
143
144 pub async fn find_service(
146 &self,
147 opts: crate::types::FindServiceOptions,
148 ) -> Result<Vec<crate::types::FindServiceResult>, JsonRpcError> {
149 self.v3_client.call_v3("find-service", json!(opts)).await
150 }
151
152 pub async fn consensus_status(
158 &self,
159 opts: crate::types::ConsensusStatusOptions,
160 ) -> Result<crate::types::V3ConsensusStatus, JsonRpcError> {
161 self.v3_client.call_v3("consensus-status", json!(opts)).await
162 }
163
164 pub async fn network_status(
170 &self,
171 opts: crate::types::NetworkStatusOptions,
172 ) -> Result<crate::types::V3NetworkStatus, JsonRpcError> {
173 self.v3_client.call_v3("network-status", json!(opts)).await
174 }
175
176 pub async fn metrics(
182 &self,
183 opts: crate::types::MetricsOptions,
184 ) -> Result<crate::types::V3Metrics, JsonRpcError> {
185 self.v3_client.call_v3("metrics", json!(opts)).await
186 }
187
188 pub async fn validate(
195 &self,
196 envelope: &TransactionEnvelope,
197 opts: crate::types::ValidateOptions,
198 ) -> Result<Vec<crate::types::V3Submission>, JsonRpcError> {
199 let request = json!({
200 "envelope": envelope,
201 "options": opts
202 });
203 self.v3_client.call_v3("validate", request).await
204 }
205
206 pub async fn list_snapshots(
212 &self,
213 opts: crate::types::ListSnapshotsOptions,
214 ) -> Result<Vec<crate::types::V3SnapshotInfo>, JsonRpcError> {
215 self.v3_client.call_v3("list-snapshots", json!(opts)).await
216 }
217
218 pub async fn submit_with_options(
224 &self,
225 envelope: &TransactionEnvelope,
226 opts: crate::types::SubmitOptions,
227 ) -> Result<Vec<crate::types::V3Submission>, JsonRpcError> {
228 let request = json!({
229 "envelope": envelope,
230 "options": opts
231 });
232 self.v3_client.call_v3("submit", request).await
233 }
234
235 pub async fn faucet_v3(
237 &self,
238 account_url: &str,
239 opts: crate::types::V3FaucetOptions,
240 ) -> Result<crate::types::V3Submission, JsonRpcError> {
241 let params = json!({
242 "account": account_url,
243 "options": opts
244 });
245 self.v3_client.call_v3("faucet", params).await
246 }
247
248 pub async fn query_advanced(
254 &self,
255 url: &str,
256 query: &crate::types::V3Query,
257 ) -> Result<QueryResponse<Value>, JsonRpcError> {
258 query.validate().map_err(|e| {
260 JsonRpcError::General(anyhow::anyhow!("Query validation failed: {}", e))
261 })?;
262
263 let params = json!({
264 "url": url,
265 "query": query
266 });
267 self.v3_client.call_v3("query", params).await
268 }
269
270 pub async fn query_chain(
272 &self,
273 url: &str,
274 query: crate::types::ChainQuery,
275 ) -> Result<QueryResponse<Value>, JsonRpcError> {
276 query.validate().map_err(|e| {
277 JsonRpcError::General(anyhow::anyhow!("Query validation failed: {}", e))
278 })?;
279
280 let params = json!({
281 "url": url,
282 "query": {
283 "queryType": "chain",
284 "name": query.name,
285 "index": query.index,
286 "entry": query.entry,
287 "range": query.range,
288 "includeReceipt": query.include_receipt
289 }
290 });
291 self.v3_client.call_v3("query", params).await
292 }
293
294 pub async fn query_data(
296 &self,
297 url: &str,
298 query: crate::types::DataQuery,
299 ) -> Result<QueryResponse<Value>, JsonRpcError> {
300 query.validate().map_err(|e| {
301 JsonRpcError::General(anyhow::anyhow!("Query validation failed: {}", e))
302 })?;
303
304 let params = json!({
305 "url": url,
306 "query": {
307 "queryType": "data",
308 "index": query.index,
309 "entry": query.entry,
310 "range": query.range
311 }
312 });
313 self.v3_client.call_v3("query", params).await
314 }
315
316 pub async fn query_directory(
318 &self,
319 url: &str,
320 query: crate::types::DirectoryQuery,
321 ) -> Result<QueryResponse<Value>, JsonRpcError> {
322 let params = json!({
323 "url": url,
324 "query": {
325 "queryType": "directory",
326 "range": query.range
327 }
328 });
329 self.v3_client.call_v3("query", params).await
330 }
331
332 pub async fn query_pending(
334 &self,
335 url: &str,
336 query: crate::types::PendingQuery,
337 ) -> Result<QueryResponse<Value>, JsonRpcError> {
338 let params = json!({
339 "url": url,
340 "query": {
341 "queryType": "pending",
342 "range": query.range
343 }
344 });
345 self.v3_client.call_v3("query", params).await
346 }
347
348 pub async fn query_block_v3(
350 &self,
351 url: &str,
352 query: crate::types::BlockQuery,
353 ) -> Result<QueryResponse<Value>, JsonRpcError> {
354 query.validate().map_err(|e| {
355 JsonRpcError::General(anyhow::anyhow!("Query validation failed: {}", e))
356 })?;
357
358 let params = json!({
359 "url": url,
360 "query": {
361 "queryType": "block",
362 "minor": query.minor,
363 "major": query.major,
364 "minorRange": query.minor_range,
365 "majorRange": query.major_range,
366 "entryRange": query.entry_range,
367 "omitEmpty": query.omit_empty
368 }
369 });
370 self.v3_client.call_v3("query", params).await
371 }
372
373 pub async fn search_anchor(
379 &self,
380 query: crate::types::AnchorSearchQuery,
381 ) -> Result<QueryResponse<Value>, JsonRpcError> {
382 let params = json!({
383 "query": {
384 "queryType": "anchorSearch",
385 "anchor": query.anchor,
386 "includeReceipt": query.include_receipt
387 }
388 });
389 self.v3_client.call_v3("query", params).await
390 }
391
392 pub async fn search_public_key(
394 &self,
395 query: crate::types::PublicKeySearchQuery,
396 ) -> Result<QueryResponse<Value>, JsonRpcError> {
397 let params = json!({
398 "query": {
399 "queryType": "publicKeySearch",
400 "publicKey": query.public_key,
401 "type": query.signature_type
402 }
403 });
404 self.v3_client.call_v3("query", params).await
405 }
406
407 pub async fn search_public_key_hash(
409 &self,
410 query: crate::types::PublicKeyHashSearchQuery,
411 ) -> Result<QueryResponse<Value>, JsonRpcError> {
412 let params = json!({
413 "query": {
414 "queryType": "publicKeyHashSearch",
415 "publicKeyHash": query.public_key_hash
416 }
417 });
418 self.v3_client.call_v3("query", params).await
419 }
420
421 pub async fn search_delegate(
423 &self,
424 query: crate::types::DelegateSearchQuery,
425 ) -> Result<QueryResponse<Value>, JsonRpcError> {
426 let params = json!({
427 "query": {
428 "queryType": "delegateSearch",
429 "delegate": query.delegate
430 }
431 });
432 self.v3_client.call_v3("query", params).await
433 }
434
435 pub async fn search_message_hash(
437 &self,
438 query: crate::types::MessageHashSearchQuery,
439 ) -> Result<QueryResponse<Value>, JsonRpcError> {
440 let params = json!({
441 "query": {
442 "queryType": "messageHashSearch",
443 "hash": query.hash
444 }
445 });
446 self.v3_client.call_v3("query", params).await
447 }
448
449 pub fn create_envelope(
454 &self,
455 tx_body: &Value,
456 keypair: &SigningKey,
457 ) -> Result<TransactionEnvelope, JsonRpcError> {
458 let timestamp = SystemTime::now()
460 .duration_since(UNIX_EPOCH)
461 .map_err(|e| JsonRpcError::General(anyhow::anyhow!("Time error: {}", e)))?
462 .as_micros() as i64;
463
464 let tx_with_timestamp = json!({
466 "body": tx_body,
467 "timestamp": timestamp
468 });
469
470 let canonical = canonical_json(&tx_with_timestamp);
472
473 let mut hasher = Sha256::new();
475 hasher.update(canonical.as_bytes());
476 let hash = hasher.finalize();
477
478 let signature = keypair.sign(&hash);
480
481 let v3_sig = V3Signature {
483 public_key: keypair.verifying_key().to_bytes().to_vec(),
484 signature: signature.to_bytes().to_vec(),
485 timestamp,
486 vote: None,
487 };
488
489 Ok(TransactionEnvelope {
490 transaction: tx_with_timestamp,
491 signatures: vec![v3_sig],
492 metadata: None,
493 })
494 }
495
496 pub fn create_envelope_binary_compatible(
510 &self,
511 principal: String,
512 tx_body: &Value,
513 keypair: &SigningKey,
514 ) -> Result<CodecTransactionEnvelope, JsonRpcError> {
515 let timestamp = SystemTime::now()
517 .duration_since(UNIX_EPOCH)
518 .map_err(|e| JsonRpcError::General(anyhow::anyhow!("Time error: {}", e)))?
519 .as_micros() as u64;
520
521 let mut envelope = TransactionCodec::create_envelope(principal, tx_body.clone(), Some(timestamp));
523
524 let hash = TransactionCodec::get_transaction_hash(&envelope)
526 .map_err(|e| JsonRpcError::General(anyhow::anyhow!("Hash error: {:?}", e)))?;
527
528 let signature = keypair.sign(&hash);
530
531 let codec_sig = TransactionSignature {
533 signature: signature.to_bytes().to_vec(),
534 signer: envelope.header.principal.clone(), timestamp,
536 vote: None,
537 public_key: Some(keypair.verifying_key().to_bytes().to_vec()),
538 key_page: None,
539 };
540
541 envelope.signatures.push(codec_sig);
543
544 Ok(envelope)
545 }
546
547 pub fn encode_envelope(&self, envelope: &CodecTransactionEnvelope) -> Result<Vec<u8>, JsonRpcError> {
549 TransactionCodec::encode_envelope(envelope)
550 .map_err(|e| JsonRpcError::General(anyhow::anyhow!("Encoding error: {:?}", e)))
551 }
552
553 pub fn decode_envelope(&self, data: &[u8]) -> Result<CodecTransactionEnvelope, JsonRpcError> {
555 TransactionCodec::decode_envelope(data)
556 .map_err(|e| JsonRpcError::General(anyhow::anyhow!("Decoding error: {:?}", e)))
557 }
558
559
560 pub fn generate_keypair() -> SigningKey {
563 use crate::crypto::ed25519::Ed25519Signer;
565 let signer = Ed25519Signer::generate();
566 SigningKey::from_bytes(&signer.private_key_bytes())
567 }
568
569 pub fn keypair_from_seed(seed: &[u8; 32]) -> Result<SigningKey, JsonRpcError> {
572 Ok(SigningKey::from_bytes(seed))
573 }
574
575 pub fn get_urls(&self) -> (String, String) {
579 (
580 self.v2_client.base_url.to_string(),
581 self.v3_client.base_url.to_string(),
582 )
583 }
584
585 pub fn validate_account_url(url: &str) -> bool {
587 url.starts_with("acc://") || url.contains('/')
589 }
590
591 pub fn create_token_transfer(
593 &self,
594 from: &str,
595 to: &str,
596 amount: u64,
597 token_url: Option<&str>,
598 ) -> Value {
599 json!({
600 "type": "sendTokens",
601 "data": {
602 "from": from,
603 "to": to,
604 "amount": amount.to_string(),
605 "token": token_url.unwrap_or("acc://ACME")
606 }
607 })
608 }
609
610 pub fn create_account(&self, url: &str, public_key: &[u8], _account_type: &str) -> Value {
612 json!({
613 "type": "createIdentity",
614 "data": {
615 "url": url,
616 "keyBook": {
617 "publicKeyHash": hex::encode(public_key)
618 },
619 "keyPage": {
620 "keys": [{
621 "publicKeyHash": hex::encode(public_key)
622 }]
623 }
624 }
625 })
626 }
627}
628
629#[cfg(test)]
632mod tests {
633 use super::*;
634 use url::Url;
635
636 #[tokio::test]
637 async fn test_client_creation() {
638 let v2_url = Url::parse("http://localhost:26660/v2").unwrap();
639 let v3_url = Url::parse("http://localhost:26661/v3").unwrap();
640 let options = AccOptions::default();
641
642 let client = AccumulateClient::new_with_options(v2_url, v3_url, options).await;
643 assert!(client.is_ok());
644 }
645
646 #[test]
647 fn test_keypair_generation() {
648 let keypair = AccumulateClient::generate_keypair();
649 assert_eq!(keypair.verifying_key().to_bytes().len(), 32);
651 assert_eq!(keypair.to_bytes().len(), 32);
652 }
653
654 #[test]
655 fn test_validate_account_url() {
656 assert!(AccumulateClient::validate_account_url("acc://test"));
657 assert!(AccumulateClient::validate_account_url("test/account"));
658 assert!(!AccumulateClient::validate_account_url("invalid"));
659 }
660
661 #[test]
662 fn test_create_token_transfer() {
663 let client_result = AccumulateClient::new_with_options(
664 Url::parse("http://localhost:26660/v2").unwrap(),
665 Url::parse("http://localhost:26661/v3").unwrap(),
666 AccOptions::default(),
667 );
668
669 let tx = serde_json::json!({
671 "type": "sendTokens",
672 "data": {
673 "from": "acc://alice",
674 "to": "acc://bob",
675 "amount": "100",
676 "token": "acc://ACME"
677 }
678 });
679
680 assert_eq!(tx["type"], "sendTokens");
681 assert_eq!(tx["data"]["amount"], "100");
682 }
683}