#![allow(missing_docs)]
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::unused_async)]
use crate::json_rpc_client::{canonical_json, JsonRpcClient, JsonRpcError};
use crate::types::*;
use crate::codec::{TransactionCodec, TransactionEnvelope as CodecTransactionEnvelope, TransactionSignature};
use crate::AccOptions;
use anyhow::Result;
use ed25519_dalek::{SigningKey, Signer};
use reqwest::Client;
use serde_json::{json, Value};
use sha2::{Digest, Sha256};
use std::time::{SystemTime, UNIX_EPOCH};
use url::Url;
#[derive(Debug, Clone)]
pub struct AccumulateClient {
pub v2_client: JsonRpcClient,
pub v3_client: JsonRpcClient,
pub options: AccOptions,
}
impl AccumulateClient {
pub async fn new_with_options(
v2_url: Url,
v3_url: Url,
options: AccOptions,
) -> Result<Self, JsonRpcError> {
let mut client_builder = Client::builder().timeout(options.timeout);
if !options.headers.is_empty() {
let mut headers = reqwest::header::HeaderMap::new();
for (key, value) in &options.headers {
let header_name =
reqwest::header::HeaderName::from_bytes(key.as_bytes()).map_err(|e| {
JsonRpcError::General(anyhow::anyhow!("Invalid header name: {}", e))
})?;
let header_value = reqwest::header::HeaderValue::from_str(value).map_err(|e| {
JsonRpcError::General(anyhow::anyhow!("Invalid header value: {}", e))
})?;
headers.insert(header_name, header_value);
}
client_builder = client_builder.default_headers(headers);
}
let http_client = client_builder.build()?;
let v2_client = JsonRpcClient::with_client(v2_url, http_client.clone())?;
let v3_client = JsonRpcClient::with_client(v3_url, http_client)?;
Ok(Self {
v2_client,
v3_client,
options,
})
}
pub async fn status(&self) -> Result<StatusResponse, JsonRpcError> {
self.v2_client.call_v2("status", None).await
}
pub async fn query_tx(&self, hash: &str) -> Result<TransactionResponse, JsonRpcError> {
self.v2_client.call_v2(&format!("tx/{}", hash), None).await
}
pub async fn query_account(&self, url: &str) -> Result<Account, JsonRpcError> {
self.v2_client.call_v2(&format!("acc/{}", url), None).await
}
pub async fn faucet(&self, account_url: &str) -> Result<FaucetResponse, JsonRpcError> {
let payload = json!({
"account": account_url
});
self.v2_client.call_v2("faucet", Some(payload)).await
}
pub async fn submit_v2(&self, tx: &Value) -> Result<TransactionResponse, JsonRpcError> {
self.v2_client.call_v2("tx", Some(tx.clone())).await
}
pub async fn submit(
&self,
envelope: &TransactionEnvelope,
) -> Result<V3SubmitResponse, JsonRpcError> {
let request = V3SubmitRequest {
envelope: envelope.clone(),
};
self.v3_client.call_v3("submit", json!(request)).await
}
pub async fn submit_multi(
&self,
envelopes: &[TransactionEnvelope],
) -> Result<Vec<V3SubmitResponse>, JsonRpcError> {
let requests: Vec<V3SubmitRequest> = envelopes
.iter()
.map(|env| V3SubmitRequest {
envelope: env.clone(),
})
.collect();
self.v3_client.call_v3("submitMulti", json!(requests)).await
}
pub async fn query(&self, url: &str) -> Result<QueryResponse<Account>, JsonRpcError> {
let params = json!({ "url": url });
self.v3_client.call_v3("query", params).await
}
pub async fn query_block(&self, height: i64) -> Result<QueryResponse<Value>, JsonRpcError> {
let params = json!({ "height": height });
self.v3_client.call_v3("queryBlock", params).await
}
pub async fn node_info(
&self,
opts: crate::types::NodeInfoOptions,
) -> Result<crate::types::V3NodeInfo, JsonRpcError> {
self.v3_client.call_v3("node-info", json!(opts)).await
}
pub async fn find_service(
&self,
opts: crate::types::FindServiceOptions,
) -> Result<Vec<crate::types::FindServiceResult>, JsonRpcError> {
self.v3_client.call_v3("find-service", json!(opts)).await
}
pub async fn consensus_status(
&self,
opts: crate::types::ConsensusStatusOptions,
) -> Result<crate::types::V3ConsensusStatus, JsonRpcError> {
self.v3_client.call_v3("consensus-status", json!(opts)).await
}
pub async fn network_status(
&self,
opts: crate::types::NetworkStatusOptions,
) -> Result<crate::types::V3NetworkStatus, JsonRpcError> {
self.v3_client.call_v3("network-status", json!(opts)).await
}
pub async fn metrics(
&self,
opts: crate::types::MetricsOptions,
) -> Result<crate::types::V3Metrics, JsonRpcError> {
self.v3_client.call_v3("metrics", json!(opts)).await
}
pub async fn validate(
&self,
envelope: &TransactionEnvelope,
opts: crate::types::ValidateOptions,
) -> Result<Vec<crate::types::V3Submission>, JsonRpcError> {
let request = json!({
"envelope": envelope,
"options": opts
});
self.v3_client.call_v3("validate", request).await
}
pub async fn list_snapshots(
&self,
opts: crate::types::ListSnapshotsOptions,
) -> Result<Vec<crate::types::V3SnapshotInfo>, JsonRpcError> {
self.v3_client.call_v3("list-snapshots", json!(opts)).await
}
pub async fn submit_with_options(
&self,
envelope: &TransactionEnvelope,
opts: crate::types::SubmitOptions,
) -> Result<Vec<crate::types::V3Submission>, JsonRpcError> {
let request = json!({
"envelope": envelope,
"options": opts
});
self.v3_client.call_v3("submit", request).await
}
pub async fn faucet_v3(
&self,
account_url: &str,
opts: crate::types::V3FaucetOptions,
) -> Result<crate::types::V3Submission, JsonRpcError> {
let params = json!({
"account": account_url,
"options": opts
});
self.v3_client.call_v3("faucet", params).await
}
pub async fn query_advanced(
&self,
url: &str,
query: &crate::types::V3Query,
) -> Result<QueryResponse<Value>, JsonRpcError> {
query.validate().map_err(|e| {
JsonRpcError::General(anyhow::anyhow!("Query validation failed: {}", e))
})?;
let params = json!({
"url": url,
"query": query
});
self.v3_client.call_v3("query", params).await
}
pub async fn query_chain(
&self,
url: &str,
query: crate::types::ChainQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
query.validate().map_err(|e| {
JsonRpcError::General(anyhow::anyhow!("Query validation failed: {}", e))
})?;
let params = json!({
"url": url,
"query": {
"queryType": "chain",
"name": query.name,
"index": query.index,
"entry": query.entry,
"range": query.range,
"includeReceipt": query.include_receipt
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn query_data(
&self,
url: &str,
query: crate::types::DataQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
query.validate().map_err(|e| {
JsonRpcError::General(anyhow::anyhow!("Query validation failed: {}", e))
})?;
let params = json!({
"url": url,
"query": {
"queryType": "data",
"index": query.index,
"entry": query.entry,
"range": query.range
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn query_directory(
&self,
url: &str,
query: crate::types::DirectoryQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
let params = json!({
"url": url,
"query": {
"queryType": "directory",
"range": query.range
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn query_pending(
&self,
url: &str,
query: crate::types::PendingQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
let params = json!({
"url": url,
"query": {
"queryType": "pending",
"range": query.range
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn query_block_v3(
&self,
url: &str,
query: crate::types::BlockQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
query.validate().map_err(|e| {
JsonRpcError::General(anyhow::anyhow!("Query validation failed: {}", e))
})?;
let params = json!({
"url": url,
"query": {
"queryType": "block",
"minor": query.minor,
"major": query.major,
"minorRange": query.minor_range,
"majorRange": query.major_range,
"entryRange": query.entry_range,
"omitEmpty": query.omit_empty
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn search_anchor(
&self,
query: crate::types::AnchorSearchQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
let params = json!({
"query": {
"queryType": "anchorSearch",
"anchor": query.anchor,
"includeReceipt": query.include_receipt
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn search_public_key(
&self,
query: crate::types::PublicKeySearchQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
let params = json!({
"query": {
"queryType": "publicKeySearch",
"publicKey": query.public_key,
"type": query.signature_type
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn search_public_key_hash(
&self,
query: crate::types::PublicKeyHashSearchQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
let params = json!({
"query": {
"queryType": "publicKeyHashSearch",
"publicKeyHash": query.public_key_hash
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn search_delegate(
&self,
query: crate::types::DelegateSearchQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
let params = json!({
"query": {
"queryType": "delegateSearch",
"delegate": query.delegate
}
});
self.v3_client.call_v3("query", params).await
}
pub async fn search_message_hash(
&self,
query: crate::types::MessageHashSearchQuery,
) -> Result<QueryResponse<Value>, JsonRpcError> {
let params = json!({
"query": {
"queryType": "messageHashSearch",
"hash": query.hash
}
});
self.v3_client.call_v3("query", params).await
}
pub fn create_envelope(
&self,
tx_body: &Value,
keypair: &SigningKey,
) -> Result<TransactionEnvelope, JsonRpcError> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| JsonRpcError::General(anyhow::anyhow!("Time error: {}", e)))?
.as_micros() as i64;
let tx_with_timestamp = json!({
"body": tx_body,
"timestamp": timestamp
});
let canonical = canonical_json(&tx_with_timestamp);
let mut hasher = Sha256::new();
hasher.update(canonical.as_bytes());
let hash = hasher.finalize();
let signature = keypair.sign(&hash);
let v3_sig = V3Signature {
public_key: keypair.verifying_key().to_bytes().to_vec(),
signature: signature.to_bytes().to_vec(),
timestamp,
vote: None,
};
Ok(TransactionEnvelope {
transaction: tx_with_timestamp,
signatures: vec![v3_sig],
metadata: None,
})
}
pub fn create_envelope_binary_compatible(
&self,
principal: String,
tx_body: &Value,
keypair: &SigningKey,
) -> Result<CodecTransactionEnvelope, JsonRpcError> {
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|e| JsonRpcError::General(anyhow::anyhow!("Time error: {}", e)))?
.as_micros() as u64;
let mut envelope = TransactionCodec::create_envelope(principal, tx_body.clone(), Some(timestamp));
let hash = TransactionCodec::get_transaction_hash(&envelope)
.map_err(|e| JsonRpcError::General(anyhow::anyhow!("Hash error: {:?}", e)))?;
let signature = keypair.sign(&hash);
let codec_sig = TransactionSignature {
signature: signature.to_bytes().to_vec(),
signer: envelope.header.principal.clone(), timestamp,
vote: None,
public_key: Some(keypair.verifying_key().to_bytes().to_vec()),
key_page: None,
};
envelope.signatures.push(codec_sig);
Ok(envelope)
}
pub fn encode_envelope(&self, envelope: &CodecTransactionEnvelope) -> Result<Vec<u8>, JsonRpcError> {
TransactionCodec::encode_envelope(envelope)
.map_err(|e| JsonRpcError::General(anyhow::anyhow!("Encoding error: {:?}", e)))
}
pub fn decode_envelope(&self, data: &[u8]) -> Result<CodecTransactionEnvelope, JsonRpcError> {
TransactionCodec::decode_envelope(data)
.map_err(|e| JsonRpcError::General(anyhow::anyhow!("Decoding error: {:?}", e)))
}
pub fn generate_keypair() -> SigningKey {
use crate::crypto::ed25519::Ed25519Signer;
let signer = Ed25519Signer::generate();
SigningKey::from_bytes(&signer.private_key_bytes())
}
pub fn keypair_from_seed(seed: &[u8; 32]) -> Result<SigningKey, JsonRpcError> {
Ok(SigningKey::from_bytes(seed))
}
pub fn get_urls(&self) -> (String, String) {
(
self.v2_client.base_url.to_string(),
self.v3_client.base_url.to_string(),
)
}
pub fn validate_account_url(url: &str) -> bool {
url.starts_with("acc://") || url.contains('/')
}
pub fn create_token_transfer(
&self,
from: &str,
to: &str,
amount: u64,
token_url: Option<&str>,
) -> Value {
json!({
"type": "sendTokens",
"data": {
"from": from,
"to": to,
"amount": amount.to_string(),
"token": token_url.unwrap_or("acc://ACME")
}
})
}
pub fn create_account(&self, url: &str, public_key: &[u8], _account_type: &str) -> Value {
json!({
"type": "createIdentity",
"data": {
"url": url,
"keyBook": {
"publicKeyHash": hex::encode(public_key)
},
"keyPage": {
"keys": [{
"publicKeyHash": hex::encode(public_key)
}]
}
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use url::Url;
#[tokio::test]
async fn test_client_creation() {
let v2_url = Url::parse("http://localhost:26660/v2").unwrap();
let v3_url = Url::parse("http://localhost:26661/v3").unwrap();
let options = AccOptions::default();
let client = AccumulateClient::new_with_options(v2_url, v3_url, options).await;
assert!(client.is_ok());
}
#[test]
fn test_keypair_generation() {
let keypair = AccumulateClient::generate_keypair();
assert_eq!(keypair.verifying_key().to_bytes().len(), 32);
assert_eq!(keypair.to_bytes().len(), 32);
}
#[test]
fn test_validate_account_url() {
assert!(AccumulateClient::validate_account_url("acc://test"));
assert!(AccumulateClient::validate_account_url("test/account"));
assert!(!AccumulateClient::validate_account_url("invalid"));
}
#[test]
fn test_create_token_transfer() {
let client_result = AccumulateClient::new_with_options(
Url::parse("http://localhost:26660/v2").unwrap(),
Url::parse("http://localhost:26661/v3").unwrap(),
AccOptions::default(),
);
let tx = serde_json::json!({
"type": "sendTokens",
"data": {
"from": "acc://alice",
"to": "acc://bob",
"amount": "100",
"token": "acc://ACME"
}
});
assert_eq!(tx["type"], "sendTokens");
assert_eq!(tx["data"]["amount"], "100");
}
}