Skip to main content

aelf_client/
lib.rs

1//! Async HTTP client, DTOs and transaction builder primitives for AElf.
2
3#![forbid(unsafe_code)]
4
5pub mod config;
6pub mod dto;
7pub mod error;
8#[doc(hidden)]
9pub mod protobuf;
10pub mod provider;
11
12#[cfg(test)]
13mod tests;
14
15pub use crate::error::AElfError;
16
17#[cfg(feature = "native-http")]
18use crate::config::ClientConfig;
19use crate::dto::{
20    BlockDto, CalculateTransactionFeeInput, CalculateTransactionFeeOutput, ChainStatusDto,
21    CreateRawTransactionInput, CreateRawTransactionOutput, ExecuteRawTransactionDto, MerklePathDto,
22    NetworkInfoOutput, PeerDto, SendRawTransactionInput, SendRawTransactionOutput,
23    SendTransactionOutput, TaskQueueInfoDto, TransactionPoolStatusOutput, TransactionResultDto,
24};
25use crate::protobuf::RawBytesMessage;
26#[cfg(feature = "native-http")]
27use crate::provider::HttpProvider;
28use crate::provider::Provider;
29use aelf_crypto::{
30    address_from_public_key, address_to_pb, base58_to_chain_id, decode_address, pb_to_address,
31    sha256_bytes, sign_transaction, Wallet,
32};
33use aelf_proto::aelf::{Address, Hash, Transaction};
34use base64::Engine;
35use http::Method;
36use prost::Message;
37use std::fmt;
38use std::sync::Arc;
39use zeroize::Zeroize;
40
41const API_BASE: &str = "api/blockChain";
42const NET_API_BASE: &str = "api/net";
43const READONLY_PRIVATE_KEY: &str =
44    "0000000000000000000000000000000000000000000000000000000000000001";
45
46fn strip_transaction_id_quotes(value: &str) -> &str {
47    let trimmed = value.trim();
48    trimmed
49        .strip_prefix('"')
50        .and_then(|unquoted| unquoted.strip_suffix('"'))
51        .unwrap_or(trimmed)
52}
53
54fn is_valid_transaction_id(value: &str) -> bool {
55    let candidate = strip_transaction_id_quotes(value);
56    let candidate = candidate
57        .strip_prefix("0x")
58        .or_else(|| candidate.strip_prefix("0X"))
59        .unwrap_or(candidate);
60    candidate.len() == 64 && candidate.chars().all(|char| char.is_ascii_hexdigit())
61}
62
63fn validate_transaction_id(
64    transaction_id: impl Into<String>,
65    raw_response: &str,
66) -> Result<String, AElfError> {
67    let transaction_id = transaction_id.into();
68    let transaction_id = strip_transaction_id_quotes(&transaction_id).to_owned();
69    if transaction_id.is_empty() {
70        return Err(AElfError::UnexpectedResponse(
71            "empty sendTransaction response".to_owned(),
72        ));
73    }
74    if is_valid_transaction_id(&transaction_id) {
75        Ok(transaction_id)
76    } else {
77        Err(AElfError::UnexpectedResponse(format!(
78            "sendTransaction returned a non-transaction id payload: {raw_response}"
79        )))
80    }
81}
82
83/// Async HTTP client used by the facade crate and lower-level integrations.
84#[derive(Clone)]
85pub struct AElfClient {
86    provider: Arc<dyn Provider>,
87}
88
89impl AElfClient {
90    /// Creates a client backed by the default HTTP provider.
91    #[cfg(feature = "native-http")]
92    pub fn new(config: ClientConfig) -> Result<Self, AElfError> {
93        Self::with_provider(HttpProvider::new(config)?)
94    }
95
96    /// Creates a client from a custom provider implementation.
97    pub fn with_provider<P>(provider: P) -> Result<Self, AElfError>
98    where
99        P: Provider + 'static,
100    {
101        Ok(Self {
102            provider: Arc::new(provider),
103        })
104    }
105
106    /// Returns block-related API helpers.
107    pub fn block(&self) -> BlockService {
108        BlockService {
109            client: self.clone(),
110        }
111    }
112
113    /// Returns chain-related API helpers.
114    pub fn chain(&self) -> ChainService {
115        ChainService {
116            client: self.clone(),
117        }
118    }
119
120    /// Returns network-related API helpers.
121    pub fn net(&self) -> NetService {
122        NetService {
123            client: self.clone(),
124        }
125    }
126
127    /// Returns transaction-related API helpers.
128    pub fn tx(&self) -> TransactionService {
129        TransactionService {
130            client: self.clone(),
131        }
132    }
133
134    /// Returns local utility helpers for transaction and address workflows.
135    pub fn utils(&self) -> ClientUtilsService {
136        ClientUtilsService {
137            client: self.clone(),
138        }
139    }
140
141    /// Creates a transaction builder bound to this client.
142    pub fn transaction_builder(&self) -> TransactionBuilder {
143        TransactionBuilder::new(self.clone())
144    }
145
146    async fn get_json<T>(&self, path: &str, query: &[(&str, String)]) -> Result<T, AElfError>
147    where
148        T: serde::de::DeserializeOwned,
149    {
150        let value = self
151            .provider
152            .request_json(Method::GET, path, query, None)
153            .await?;
154        serde_json::from_value(value).map_err(AElfError::Json)
155    }
156
157    async fn post_json<T>(&self, path: &str, body: serde_json::Value) -> Result<T, AElfError>
158    where
159        T: serde::de::DeserializeOwned,
160    {
161        let value = self
162            .provider
163            .request_json(Method::POST, path, &[], Some(body))
164            .await?;
165        serde_json::from_value(value).map_err(AElfError::Json)
166    }
167
168    async fn get_text(&self, path: &str, query: &[(&str, String)]) -> Result<String, AElfError> {
169        self.provider
170            .request_text(Method::GET, path, query, None)
171            .await
172    }
173
174    async fn post_text(&self, path: &str, body: serde_json::Value) -> Result<String, AElfError> {
175        self.provider
176            .request_text(Method::POST, path, &[], Some(body))
177            .await
178    }
179}
180
181#[derive(Clone)]
182pub struct BlockService {
183    client: AElfClient,
184}
185
186impl BlockService {
187    pub async fn get_block_height(&self) -> Result<i64, AElfError> {
188        let text = self
189            .client
190            .get_text(&format!("{API_BASE}/blockHeight"), &[])
191            .await?;
192        text.trim_matches('"')
193            .parse::<i64>()
194            .map_err(|err| AElfError::InvalidConfig(format!("invalid block height: {err}")))
195    }
196
197    pub async fn get_block_by_hash(
198        &self,
199        block_hash: &str,
200        include_transactions: bool,
201    ) -> Result<BlockDto, AElfError> {
202        self.client
203            .get_json(
204                &format!("{API_BASE}/block"),
205                &[
206                    ("blockHash", block_hash.to_owned()),
207                    ("includeTransactions", include_transactions.to_string()),
208                ],
209            )
210            .await
211    }
212
213    pub async fn get_block_by_height(
214        &self,
215        block_height: i64,
216        include_transactions: bool,
217    ) -> Result<BlockDto, AElfError> {
218        self.client
219            .get_json(
220                &format!("{API_BASE}/blockByHeight"),
221                &[
222                    ("blockHeight", block_height.to_string()),
223                    ("includeTransactions", include_transactions.to_string()),
224                ],
225            )
226            .await
227    }
228}
229
230#[derive(Clone)]
231pub struct ChainService {
232    client: AElfClient,
233}
234
235impl ChainService {
236    pub async fn get_chain_status(&self) -> Result<ChainStatusDto, AElfError> {
237        self.client
238            .get_json(&format!("{API_BASE}/chainStatus"), &[])
239            .await
240    }
241
242    pub async fn get_contract_file_descriptor_set(
243        &self,
244        address: &str,
245    ) -> Result<Vec<u8>, AElfError> {
246        let text = self
247            .client
248            .get_text(
249                &format!("{API_BASE}/contractFileDescriptorSet"),
250                &[("address", address.to_owned())],
251            )
252            .await?;
253        Ok(base64::engine::general_purpose::STANDARD.decode(text.trim_matches('"'))?)
254    }
255
256    pub async fn get_task_queue_status(&self) -> Result<Vec<TaskQueueInfoDto>, AElfError> {
257        self.client
258            .get_json(&format!("{API_BASE}/taskQueueStatus"), &[])
259            .await
260    }
261
262    pub async fn get_chain_id(&self) -> Result<i32, AElfError> {
263        let status = self.get_chain_status().await?;
264        base58_to_chain_id(&status.chain_id).map_err(AElfError::Crypto)
265    }
266}
267
268#[derive(Clone)]
269pub struct NetService {
270    client: AElfClient,
271}
272
273impl NetService {
274    pub async fn add_peer(&self, address: &str) -> Result<bool, AElfError> {
275        self.client
276            .post_json(
277                &format!("{NET_API_BASE}/peer"),
278                serde_json::json!({ "Address": address }),
279            )
280            .await
281    }
282
283    pub async fn remove_peer(&self, address: &str) -> Result<bool, AElfError> {
284        let text = self
285            .client
286            .provider
287            .request_text(
288                Method::DELETE,
289                &format!("{NET_API_BASE}/peer"),
290                &[("address", address.to_owned())],
291                None,
292            )
293            .await?;
294        serde_json::from_str(&text).or_else(|_| Ok(text.trim().eq_ignore_ascii_case("true")))
295    }
296
297    pub async fn get_peers(&self, with_metrics: bool) -> Result<Vec<PeerDto>, AElfError> {
298        self.client
299            .get_json(
300                &format!("{NET_API_BASE}/peers"),
301                &[("withMetrics", with_metrics.to_string())],
302            )
303            .await
304    }
305
306    pub async fn get_network_info(&self) -> Result<NetworkInfoOutput, AElfError> {
307        self.client
308            .get_json(&format!("{NET_API_BASE}/networkInfo"), &[])
309            .await
310    }
311}
312
313#[derive(Clone)]
314pub struct TransactionService {
315    client: AElfClient,
316}
317
318impl TransactionService {
319    pub async fn get_transaction_pool_status(
320        &self,
321    ) -> Result<TransactionPoolStatusOutput, AElfError> {
322        self.client
323            .get_json(&format!("{API_BASE}/transactionPoolStatus"), &[])
324            .await
325    }
326
327    pub async fn execute_transaction(&self, raw_transaction: &str) -> Result<String, AElfError> {
328        self.client
329            .post_text(
330                &format!("{API_BASE}/executeTransaction"),
331                serde_json::json!({ "RawTransaction": raw_transaction }),
332            )
333            .await
334    }
335
336    pub async fn execute_raw_transaction(
337        &self,
338        input: &ExecuteRawTransactionDto,
339    ) -> Result<String, AElfError> {
340        self.client
341            .post_text(
342                &format!("{API_BASE}/executeRawTransaction"),
343                serde_json::json!({
344                    "RawTransaction": input.raw_transaction,
345                    "Signature": input.signature,
346                }),
347            )
348            .await
349    }
350
351    pub async fn create_raw_transaction(
352        &self,
353        input: &CreateRawTransactionInput,
354    ) -> Result<CreateRawTransactionOutput, AElfError> {
355        self.client
356            .post_json(
357                &format!("{API_BASE}/rawTransaction"),
358                serde_json::to_value(input)?,
359            )
360            .await
361    }
362
363    pub async fn send_raw_transaction(
364        &self,
365        input: &SendRawTransactionInput,
366    ) -> Result<SendRawTransactionOutput, AElfError> {
367        self.client
368            .post_json(
369                &format!("{API_BASE}/sendRawTransaction"),
370                serde_json::to_value(input)?,
371            )
372            .await
373    }
374
375    pub async fn send_transaction(
376        &self,
377        raw_transaction: &str,
378    ) -> Result<SendTransactionOutput, AElfError> {
379        let text = self
380            .client
381            .post_text(
382                &format!("{API_BASE}/sendTransaction"),
383                serde_json::json!({ "RawTransaction": raw_transaction }),
384            )
385            .await?;
386        if let Ok(output) = serde_json::from_str::<SendTransactionOutput>(&text) {
387            return Ok(SendTransactionOutput {
388                transaction_id: validate_transaction_id(output.transaction_id, &text)?,
389            });
390        }
391
392        if let Ok(transaction_id) = serde_json::from_str::<String>(&text) {
393            return Ok(SendTransactionOutput {
394                transaction_id: validate_transaction_id(transaction_id, &text)?,
395            });
396        }
397
398        Ok(SendTransactionOutput {
399            transaction_id: validate_transaction_id(&text, &text)?,
400        })
401    }
402
403    pub async fn send_transactions(
404        &self,
405        raw_transactions: &str,
406    ) -> Result<Vec<String>, AElfError> {
407        self.client
408            .post_json(
409                &format!("{API_BASE}/sendTransactions"),
410                serde_json::json!({ "RawTransactions": raw_transactions }),
411            )
412            .await
413    }
414
415    pub async fn get_transaction_result(
416        &self,
417        transaction_id: &str,
418    ) -> Result<TransactionResultDto, AElfError> {
419        self.client
420            .get_json(
421                &format!("{API_BASE}/transactionResult"),
422                &[("transactionId", transaction_id.to_owned())],
423            )
424            .await
425    }
426
427    pub async fn get_transaction_results(
428        &self,
429        block_hash: &str,
430        offset: i64,
431        limit: i64,
432    ) -> Result<Vec<TransactionResultDto>, AElfError> {
433        self.client
434            .get_json(
435                &format!("{API_BASE}/transactionResults"),
436                &[
437                    ("blockHash", block_hash.to_owned()),
438                    ("offset", offset.to_string()),
439                    ("limit", limit.to_string()),
440                ],
441            )
442            .await
443    }
444
445    pub async fn get_merkle_path_by_transaction_id(
446        &self,
447        transaction_id: &str,
448    ) -> Result<MerklePathDto, AElfError> {
449        self.client
450            .get_json(
451                &format!("{API_BASE}/merklePathByTransactionId"),
452                &[("transactionId", transaction_id.to_owned())],
453            )
454            .await
455    }
456
457    pub async fn calculate_transaction_fee(
458        &self,
459        input: &CalculateTransactionFeeInput,
460    ) -> Result<CalculateTransactionFeeOutput, AElfError> {
461        self.client
462            .post_json(
463                &format!("{API_BASE}/calculateTransactionFee"),
464                serde_json::to_value(input)?,
465            )
466            .await
467    }
468}
469
470#[derive(Clone)]
471pub struct ClientUtilsService {
472    client: AElfClient,
473}
474
475impl ClientUtilsService {
476    pub async fn is_connected(&self) -> bool {
477        self.client.chain().get_chain_status().await.is_ok()
478    }
479
480    pub fn get_address_from_pub_key(&self, public_key_hex: &str) -> Result<String, AElfError> {
481        let public_key = hex::decode(public_key_hex)?;
482        Ok(address_from_public_key(&public_key))
483    }
484
485    pub fn get_address_from_private_key(&self, private_key_hex: &str) -> Result<String, AElfError> {
486        let wallet = Wallet::from_private_key(private_key_hex)?;
487        Ok(wallet.address().to_owned())
488    }
489
490    pub fn generate_key_pair_info(&self) -> Result<KeyPairInfo, AElfError> {
491        let wallet = Wallet::create()?;
492        Ok(KeyPairInfo::from_wallet(&wallet))
493    }
494
495    pub async fn get_genesis_contract_address(&self) -> Result<String, AElfError> {
496        let status = self.client.chain().get_chain_status().await?;
497        Ok(status.genesis_contract_address)
498    }
499
500    pub async fn get_contract_address_by_name(
501        &self,
502        contract_name: &str,
503    ) -> Result<String, AElfError> {
504        let readonly_wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY)?;
505        let genesis_address = self.get_genesis_contract_address().await?;
506        let request = Hash {
507            value: sha256_bytes(contract_name.as_bytes()).to_vec(),
508        };
509        let transaction = self
510            .generate_transaction(
511                readonly_wallet.address(),
512                &genesis_address,
513                "GetContractAddressByName",
514                &request,
515            )
516            .await?;
517        let signed = self.sign_transaction(&readonly_wallet, transaction)?;
518        let raw = hex::encode(signed.encode_to_vec());
519        let response = self.client.tx().execute_transaction(&raw).await?;
520        let bytes = hex::decode(response.trim_matches('"'))?;
521        let address = Address::decode(bytes.as_slice())?;
522        Ok(pb_to_address(&address))
523    }
524
525    pub async fn get_formatted_address(&self, address: &str) -> Result<String, AElfError> {
526        let readonly_wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY)?;
527        let token_address = self
528            .get_contract_address_by_name("AElf.ContractNames.Token")
529            .await?;
530        let transaction = self
531            .generate_transaction(
532                readonly_wallet.address(),
533                &token_address,
534                "GetPrimaryTokenSymbol",
535                &pbjson_types::Empty {},
536            )
537            .await?;
538        let signed = self.sign_transaction(&readonly_wallet, transaction)?;
539        let raw = hex::encode(signed.encode_to_vec());
540        let response = self.client.tx().execute_transaction(&raw).await?;
541        let bytes = hex::decode(response.trim_matches('"'))?;
542        let symbol = pbjson_types::StringValue::decode(bytes.as_slice())?;
543        let status = self.client.chain().get_chain_status().await?;
544        Ok(format!("{}_{}_{}", symbol.value, address, status.chain_id))
545    }
546
547    pub async fn generate_transaction<M>(
548        &self,
549        from: &str,
550        to: &str,
551        method_name: &str,
552        input: &M,
553    ) -> Result<Transaction, AElfError>
554    where
555        M: Message,
556    {
557        let chain_status = self.client.chain().get_chain_status().await?;
558        let best_chain_hash = chain_status.best_chain_hash.trim_start_matches("0x");
559        let hash_bytes = hex::decode(best_chain_hash)?;
560        let prefix = hash_bytes
561            .get(..4)
562            .ok_or_else(|| AElfError::request("best chain hash is too short", None))?;
563        Ok(Transaction {
564            from: Some(address_to_pb(from)?),
565            to: Some(address_to_pb(to)?),
566            ref_block_number: chain_status.best_chain_height,
567            ref_block_prefix: prefix.to_vec(),
568            method_name: method_name.to_owned(),
569            params: input.encode_to_vec(),
570            signature: Vec::new(),
571        })
572    }
573
574    pub fn sign_transaction(
575        &self,
576        wallet: &Wallet,
577        mut transaction: Transaction,
578    ) -> Result<Transaction, AElfError> {
579        transaction.signature = sign_transaction(wallet, &transaction)?;
580        Ok(transaction)
581    }
582
583    pub fn decode_address(&self, address: &str) -> Result<Vec<u8>, AElfError> {
584        decode_address(address).map_err(AElfError::Crypto)
585    }
586}
587
588/// Basic key material derived from a wallet.
589///
590/// The private key is zeroized on drop and redacted from `Debug` output.
591#[derive(Clone, PartialEq, Eq)]
592pub struct KeyPairInfo {
593    pub private_key: String,
594    pub public_key: String,
595    pub address: String,
596}
597
598impl KeyPairInfo {
599    /// Builds a serializable key bundle from a wallet.
600    pub fn from_wallet(wallet: &Wallet) -> Self {
601        Self {
602            private_key: wallet.private_key().to_owned(),
603            public_key: wallet.public_key().to_owned(),
604            address: wallet.address().to_owned(),
605        }
606    }
607}
608
609impl fmt::Debug for KeyPairInfo {
610    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
611        f.debug_struct("KeyPairInfo")
612            .field("private_key", &"<redacted>")
613            .field("public_key", &self.public_key)
614            .field("address", &self.address)
615            .finish()
616    }
617}
618
619impl Drop for KeyPairInfo {
620    fn drop(&mut self) {
621        self.private_key.zeroize();
622    }
623}
624
625/// Builder for unsigned or signed AElf protobuf transactions.
626#[derive(Clone)]
627pub struct TransactionBuilder {
628    client: AElfClient,
629    wallet: Option<Wallet>,
630    contract_address: Option<String>,
631    system_contract_name: Option<String>,
632    method_name: Option<String>,
633    params: Option<Vec<u8>>,
634}
635
636impl TransactionBuilder {
637    /// Creates a new builder bound to an existing client.
638    pub fn new(client: AElfClient) -> Self {
639        Self {
640            client,
641            wallet: None,
642            contract_address: None,
643            system_contract_name: None,
644            method_name: None,
645            params: None,
646        }
647    }
648
649    /// Sets the wallet used for address resolution and signing.
650    pub fn with_wallet(mut self, wallet: Wallet) -> Self {
651        self.wallet = Some(wallet);
652        self
653    }
654
655    /// Sets the target contract address explicitly.
656    pub fn with_contract(mut self, address: impl Into<String>) -> Self {
657        self.contract_address = Some(address.into());
658        self
659    }
660
661    /// Resolves the contract address from a known system contract name.
662    pub fn with_system_contract(mut self, name: impl Into<String>) -> Self {
663        self.system_contract_name = Some(name.into());
664        self
665    }
666
667    /// Sets the target contract method name.
668    pub fn with_method(mut self, method_name: impl Into<String>) -> Self {
669        self.method_name = Some(method_name.into());
670        self
671    }
672
673    /// Encodes a protobuf message into the transaction `params` payload.
674    pub fn with_message<M: Message>(mut self, message: &M) -> Self {
675        self.params = Some(message.encode_to_vec());
676        self
677    }
678
679    /// Builds an unsigned protobuf transaction.
680    pub async fn build_unsigned(self) -> Result<Transaction, AElfError> {
681        let wallet = self.wallet.ok_or(AElfError::MissingField("wallet"))?;
682        let contract_address = match (self.contract_address, self.system_contract_name) {
683            (Some(address), _) => address,
684            (None, Some(name)) => {
685                self.client
686                    .utils()
687                    .get_contract_address_by_name(&name)
688                    .await?
689            }
690            (None, None) => return Err(AElfError::MissingField("contract address")),
691        };
692        let method_name = self
693            .method_name
694            .ok_or(AElfError::MissingField("method name"))?;
695        let params = self.params.unwrap_or_default();
696        self.client
697            .utils()
698            .generate_transaction(
699                wallet.address(),
700                &contract_address,
701                &method_name,
702                &RawBytesMessage::new(params),
703            )
704            .await
705    }
706
707    /// Builds and signs a protobuf transaction with the selected wallet.
708    pub async fn build_signed(self) -> Result<Transaction, AElfError> {
709        let wallet = self
710            .wallet
711            .clone()
712            .ok_or(AElfError::MissingField("wallet"))?;
713        let unsigned = self.clone().build_unsigned().await?;
714        self.client.utils().sign_transaction(&wallet, unsigned)
715    }
716}