Skip to main content

aelf_contract/
lib.rs

1//! Typed and dynamic contract bindings for the AElf Rust SDK.
2
3#![forbid(unsafe_code)]
4
5use aelf_client::dto::SendTransactionOutput;
6use aelf_client::protobuf::RawBytesMessage;
7use aelf_client::{AElfClient, AElfError};
8use aelf_crypto::{address_to_pb, hash_to_pb, pb_to_address, Wallet};
9use aelf_proto::{aedpos, cross_chain, election, token, vote};
10use prost::Message;
11use prost_reflect::{DescriptorPool, DynamicMessage, Kind, MessageDescriptor, MethodDescriptor};
12use serde::de::IntoDeserializer;
13use serde_json::{Map, Value};
14use std::sync::Arc;
15use thiserror::Error;
16use tokio::sync::OnceCell;
17
18/// Errors returned by typed and dynamic contract operations.
19#[derive(Debug, Error)]
20pub enum ContractError {
21    #[error("client error: {0}")]
22    Client(Box<AElfError>),
23    #[error("method not found: {0}")]
24    MethodNotFound(String),
25    #[error("descriptor error: {0}")]
26    Descriptor(#[from] prost_reflect::DescriptorError),
27    #[error("json error: {0}")]
28    Json(#[from] serde_json::Error),
29    #[error("crypto error: {0}")]
30    Crypto(#[from] aelf_crypto::CryptoError),
31    #[error("hex decode error: {0}")]
32    Hex(#[from] hex::FromHexError),
33    #[error("protobuf decode error: {0}")]
34    ProtobufDecode(#[from] prost::DecodeError),
35}
36
37impl From<AElfError> for ContractError {
38    fn from(value: AElfError) -> Self {
39        Self::Client(Box::new(value))
40    }
41}
42
43/// Contract handle backed by a descriptor pool fetched from chain.
44#[derive(Clone)]
45pub struct DynamicContract {
46    client: AElfClient,
47    address: String,
48    wallet: Wallet,
49    pool: DescriptorPool,
50}
51
52impl DynamicContract {
53    /// Loads a contract descriptor from chain and returns a dynamic contract handle.
54    pub async fn at(
55        client: AElfClient,
56        address: impl Into<String>,
57        wallet: Wallet,
58    ) -> Result<Self, ContractError> {
59        let address = address.into();
60        let bytes = client
61            .chain()
62            .get_contract_file_descriptor_set(&address)
63            .await?;
64        let pool = DescriptorPool::decode(bytes.as_slice())?;
65
66        Ok(Self {
67            client,
68            address,
69            wallet,
70            pool,
71        })
72    }
73
74    /// Returns the contract address associated with this handle.
75    pub fn address(&self) -> &str {
76        &self.address
77    }
78
79    /// Resolves a method descriptor by name.
80    pub fn method(&self, name: &str) -> Result<DynamicContractMethod, ContractError> {
81        let mut found = None;
82        for service in self.pool.services() {
83            if let Some(method) = service.methods().find(|method| method.name() == name) {
84                found = Some(method);
85                break;
86            }
87        }
88
89        let method = found.ok_or_else(|| ContractError::MethodNotFound(name.to_owned()))?;
90        Ok(DynamicContractMethod {
91            client: self.client.clone(),
92            address: self.address.clone(),
93            wallet: self.wallet.clone(),
94            method,
95        })
96    }
97
98    /// Calls a contract method with protobuf input and decodes the protobuf output.
99    pub async fn call_typed<MIn, MOut>(
100        &self,
101        method_name: &str,
102        input: &MIn,
103    ) -> Result<MOut, ContractError>
104    where
105        MIn: Message,
106        MOut: Message + Default,
107    {
108        let bytes = self
109            .method(method_name)?
110            .call_bytes(&input.encode_to_vec())
111            .await?;
112        Ok(MOut::decode(bytes.as_slice())?)
113    }
114
115    /// Sends a transaction with protobuf input and returns the broadcast response.
116    pub async fn send_typed<MIn>(
117        &self,
118        method_name: &str,
119        input: &MIn,
120    ) -> Result<SendTransactionOutput, ContractError>
121    where
122        MIn: Message,
123    {
124        self.method(method_name)?
125            .send_bytes(&input.encode_to_vec())
126            .await
127    }
128
129    /// Calls a contract method using protobuf JSON input and returns protobuf JSON output.
130    pub async fn call_json(&self, method_name: &str, input: Value) -> Result<Value, ContractError> {
131        self.method(method_name)?.call_json(input).await
132    }
133
134    /// Sends a transaction using protobuf JSON input.
135    pub async fn send_json(
136        &self,
137        method_name: &str,
138        input: Value,
139    ) -> Result<SendTransactionOutput, ContractError> {
140        self.method(method_name)?.send_json(input).await
141    }
142}
143
144/// Bound dynamic contract method descriptor.
145#[derive(Clone)]
146pub struct DynamicContractMethod {
147    client: AElfClient,
148    address: String,
149    wallet: Wallet,
150    method: MethodDescriptor,
151}
152
153impl DynamicContractMethod {
154    /// Returns the underlying reflected method descriptor.
155    pub fn descriptor(&self) -> &MethodDescriptor {
156        &self.method
157    }
158
159    /// Calls a method with already encoded protobuf bytes.
160    pub async fn call_bytes(&self, input: &[u8]) -> Result<Vec<u8>, ContractError> {
161        let raw = build_signed_raw(
162            &self.client,
163            &self.wallet,
164            &self.address,
165            self.method.name(),
166            input,
167        )
168        .await?;
169        let response = self.client.tx().execute_transaction(&raw).await?;
170        Ok(hex::decode(response.trim_matches('"'))?)
171    }
172
173    /// Sends a transaction with already encoded protobuf bytes.
174    pub async fn send_bytes(&self, input: &[u8]) -> Result<SendTransactionOutput, ContractError> {
175        let raw = build_signed_raw(
176            &self.client,
177            &self.wallet,
178            &self.address,
179            self.method.name(),
180            input,
181        )
182        .await?;
183        self.client
184            .tx()
185            .send_transaction(&raw)
186            .await
187            .map_err(Into::into)
188    }
189
190    /// Calls a method with protobuf JSON input and returns protobuf JSON output.
191    pub async fn call_json(&self, input: Value) -> Result<Value, ContractError> {
192        let input = normalize_json_input(&self.method.input(), input)?;
193        let message = DynamicMessage::deserialize(self.method.input(), input.into_deserializer())?;
194        let output = self.call_bytes(&message.encode_to_vec()).await?;
195        let output = DynamicMessage::decode(self.method.output(), output.as_slice())?;
196        let output = serde_json::to_value(output)?;
197        normalize_json_output(&self.method.output(), output)
198    }
199
200    /// Sends a transaction with protobuf JSON input.
201    pub async fn send_json(&self, input: Value) -> Result<SendTransactionOutput, ContractError> {
202        let input = normalize_json_input(&self.method.input(), input)?;
203        let message = DynamicMessage::deserialize(self.method.input(), input.into_deserializer())?;
204        self.send_bytes(&message.encode_to_vec()).await
205    }
206}
207
208#[derive(Clone, Copy, Debug, PartialEq, Eq)]
209enum JsonDirection {
210    Input,
211    Output,
212}
213
214fn normalize_json_input(desc: &MessageDescriptor, value: Value) -> Result<Value, ContractError> {
215    normalize_json_message(desc, value, JsonDirection::Input)
216}
217
218fn normalize_json_output(desc: &MessageDescriptor, value: Value) -> Result<Value, ContractError> {
219    normalize_json_message(desc, value, JsonDirection::Output)
220}
221
222fn normalize_json_message(
223    desc: &MessageDescriptor,
224    value: Value,
225    direction: JsonDirection,
226) -> Result<Value, ContractError> {
227    match (desc.full_name(), direction) {
228        ("aelf.Address", JsonDirection::Input) => {
229            return match value {
230                Value::String(address) => Ok(serde_json::to_value(address_to_pb(&address)?)?),
231                other => Ok(other),
232            };
233        }
234        ("aelf.Address", JsonDirection::Output) => {
235            return match value {
236                Value::Object(_) => {
237                    let address: aelf_proto::aelf::Address = serde_json::from_value(value)?;
238                    Ok(Value::String(pb_to_address(&address)))
239                }
240                other => Ok(other),
241            };
242        }
243        ("aelf.Hash", JsonDirection::Input) => {
244            return match value {
245                Value::String(hash) => {
246                    let bytes = hex::decode(hash)?;
247                    Ok(serde_json::to_value(hash_to_pb(bytes))?)
248                }
249                other => Ok(other),
250            };
251        }
252        ("aelf.Hash", JsonDirection::Output) => {
253            return match value {
254                Value::Object(_) => {
255                    let hash: aelf_proto::aelf::Hash = serde_json::from_value(value)?;
256                    Ok(Value::String(hex::encode(hash.value)))
257                }
258                other => Ok(other),
259            };
260        }
261        _ => {}
262    }
263
264    let Value::Object(object) = value else {
265        return Ok(value);
266    };
267
268    let mut normalized = Map::with_capacity(object.len());
269    for (key, value) in object {
270        let field = desc
271            .get_field_by_json_name(&key)
272            .or_else(|| desc.get_field_by_name(&key));
273        let value = match field {
274            Some(field) => normalize_json_field(&field, value, direction)?,
275            None => value,
276        };
277        normalized.insert(key, value);
278    }
279
280    Ok(Value::Object(normalized))
281}
282fn normalize_json_field(
283    field: &prost_reflect::FieldDescriptor,
284    value: Value,
285    direction: JsonDirection,
286) -> Result<Value, ContractError> {
287    if field.is_list() {
288        return match (field.kind(), value) {
289            (Kind::Message(desc), Value::Array(items)) => Ok(Value::Array(
290                items
291                    .into_iter()
292                    .map(|item| normalize_json_message(&desc, item, direction))
293                    .collect::<Result<Vec<_>, _>>()?,
294            )),
295            (_, other) => Ok(other),
296        };
297    }
298
299    if field.is_map() {
300        return match (field.kind(), value) {
301            (Kind::Message(entry), Value::Object(entries)) => {
302                let value_field = entry.map_entry_value_field();
303                match value_field.kind() {
304                    Kind::Message(desc) => {
305                        let mut normalized = Map::with_capacity(entries.len());
306                        for (key, value) in entries {
307                            normalized
308                                .insert(key, normalize_json_message(&desc, value, direction)?);
309                        }
310                        Ok(Value::Object(normalized))
311                    }
312                    _ => Ok(Value::Object(entries)),
313                }
314            }
315            (_, other) => Ok(other),
316        };
317    }
318
319    match field.kind() {
320        Kind::Message(desc) => normalize_json_message(&desc, value, direction),
321        _ => Ok(value),
322    }
323}
324
325async fn build_signed_raw(
326    client: &AElfClient,
327    wallet: &Wallet,
328    address: &str,
329    method_name: &str,
330    params: &[u8],
331) -> Result<String, ContractError> {
332    let transaction = client
333        .transaction_builder()
334        .with_wallet(wallet.clone())
335        .with_contract(address.to_owned())
336        .with_method(method_name.to_owned())
337        .with_message(&RawBytesMessage::new(params.to_vec()))
338        .build_signed()
339        .await?;
340    Ok(hex::encode(transaction.encode_to_vec()))
341}
342
343#[derive(Clone)]
344struct LazyDynamicContract {
345    client: AElfClient,
346    wallet: Wallet,
347    address: String,
348    dynamic: Arc<OnceCell<DynamicContract>>,
349}
350
351impl LazyDynamicContract {
352    fn new(client: AElfClient, wallet: Wallet, address: impl Into<String>) -> Self {
353        Self {
354            client,
355            wallet,
356            address: address.into(),
357            dynamic: Arc::new(OnceCell::new()),
358        }
359    }
360
361    async fn get(&self) -> Result<DynamicContract, ContractError> {
362        let contract = self
363            .dynamic
364            .get_or_try_init(|| async {
365                DynamicContract::at(
366                    self.client.clone(),
367                    self.address.clone(),
368                    self.wallet.clone(),
369                )
370                .await
371            })
372            .await?;
373        Ok(contract.clone())
374    }
375}
376
377/// Typed wrapper for the genesis zero contract.
378#[derive(Clone)]
379pub struct ZeroContract {
380    client: AElfClient,
381    wallet: Wallet,
382    address: String,
383}
384
385impl ZeroContract {
386    /// Creates a typed zero contract wrapper.
387    pub fn new(client: AElfClient, wallet: Wallet, address: impl Into<String>) -> Self {
388        Self {
389            client,
390            wallet,
391            address: address.into(),
392        }
393    }
394
395    /// Resolves a system contract address by its canonical contract name.
396    pub async fn get_contract_address_by_name(
397        &self,
398        contract_name: &str,
399    ) -> Result<String, ContractError> {
400        let input = aelf_proto::aelf::Hash {
401            value: aelf_crypto::sha256_bytes(contract_name.as_bytes()).to_vec(),
402        };
403        let raw = build_signed_raw(
404            &self.client,
405            &self.wallet,
406            &self.address,
407            "GetContractAddressByName",
408            &input.encode_to_vec(),
409        )
410        .await?;
411        let response = self.client.tx().execute_transaction(&raw).await?;
412        let output =
413            aelf_proto::aelf::Address::decode(hex::decode(response.trim_matches('"'))?.as_slice())?;
414        Ok(pb_to_address(&output))
415    }
416}
417
418/// Typed wrapper for the token contract.
419#[derive(Clone)]
420pub struct TokenContract {
421    dynamic: LazyDynamicContract,
422}
423
424impl TokenContract {
425    /// Creates a typed token contract wrapper.
426    pub fn new(client: AElfClient, wallet: Wallet, address: impl Into<String>) -> Self {
427        Self {
428            dynamic: LazyDynamicContract::new(client, wallet, address),
429        }
430    }
431
432    /// Returns the token balance for a given owner and symbol.
433    pub async fn get_balance(
434        &self,
435        input: &token::GetBalanceInput,
436    ) -> Result<token::GetBalanceOutput, ContractError> {
437        self.dynamic().await?.call_typed("GetBalance", input).await
438    }
439
440    /// Returns token metadata for a specific symbol.
441    pub async fn get_token_info(
442        &self,
443        input: &token::GetTokenInfoInput,
444    ) -> Result<token::TokenInfo, ContractError> {
445        self.dynamic()
446            .await?
447            .call_typed("GetTokenInfo", input)
448            .await
449    }
450
451    /// Returns metadata for the chain's native token.
452    pub async fn get_native_token_info(&self) -> Result<token::TokenInfo, ContractError> {
453        self.dynamic()
454            .await?
455            .call_typed("GetNativeTokenInfo", &pbjson_types::Empty {})
456            .await
457    }
458
459    /// Returns the primary token symbol used by the chain.
460    pub async fn get_primary_token_symbol(&self) -> Result<String, ContractError> {
461        let output: pbjson_types::StringValue = self
462            .dynamic()
463            .await?
464            .call_typed("GetPrimaryTokenSymbol", &pbjson_types::Empty {})
465            .await?;
466        Ok(output.value)
467    }
468
469    /// Transfers tokens to another address.
470    pub async fn transfer(
471        &self,
472        input: &token::TransferInput,
473    ) -> Result<SendTransactionOutput, ContractError> {
474        self.dynamic().await?.send_typed("Transfer", input).await
475    }
476
477    /// Sends tokens to another chain through the token contract.
478    pub async fn cross_chain_transfer(
479        &self,
480        input: &token::CrossChainTransferInput,
481    ) -> Result<SendTransactionOutput, ContractError> {
482        self.dynamic()
483            .await?
484            .send_typed("CrossChainTransfer", input)
485            .await
486    }
487
488    async fn dynamic(&self) -> Result<DynamicContract, ContractError> {
489        self.dynamic.get().await
490    }
491}
492
493/// Typed wrapper for the election contract.
494#[derive(Clone)]
495pub struct ElectionContract {
496    dynamic: LazyDynamicContract,
497}
498
499impl ElectionContract {
500    /// Creates a typed election contract wrapper.
501    pub fn new(client: AElfClient, wallet: Wallet, address: impl Into<String>) -> Self {
502        Self {
503            dynamic: LazyDynamicContract::new(client, wallet, address),
504        }
505    }
506
507    /// Returns all registered candidate public keys as hex strings.
508    pub async fn get_candidates(&self) -> Result<Vec<String>, ContractError> {
509        let output: election::PubkeyList = self
510            .dynamic()
511            .await?
512            .call_typed("GetCandidates", &pbjson_types::Empty {})
513            .await?;
514        Ok(output.value.into_iter().map(hex::encode).collect())
515    }
516
517    /// Returns vote statistics for a candidate public key.
518    pub async fn get_candidate_vote(
519        &self,
520        pubkey: &str,
521    ) -> Result<election::CandidateVote, ContractError> {
522        self.dynamic()
523            .await?
524            .call_typed(
525                "GetCandidateVote",
526                &pbjson_types::StringValue {
527                    value: pubkey.to_owned(),
528                },
529            )
530            .await
531    }
532
533    /// Returns vote information for an elector public key.
534    pub async fn get_elector_vote(
535        &self,
536        pubkey: &str,
537    ) -> Result<election::ElectorVote, ContractError> {
538        self.dynamic()
539            .await?
540            .call_typed(
541                "GetElectorVote",
542                &pbjson_types::StringValue {
543                    value: pubkey.to_owned(),
544                },
545            )
546            .await
547    }
548
549    async fn dynamic(&self) -> Result<DynamicContract, ContractError> {
550        self.dynamic.get().await
551    }
552}
553
554/// Typed wrapper for the vote contract.
555#[derive(Clone)]
556pub struct VoteContract {
557    dynamic: LazyDynamicContract,
558}
559
560impl VoteContract {
561    /// Creates a typed vote contract wrapper.
562    pub fn new(client: AElfClient, wallet: Wallet, address: impl Into<String>) -> Self {
563        Self {
564            dynamic: LazyDynamicContract::new(client, wallet, address),
565        }
566    }
567
568    /// Returns metadata for a voting item.
569    pub async fn get_voting_item(
570        &self,
571        input: &vote::GetVotingItemInput,
572    ) -> Result<vote::VotingItem, ContractError> {
573        self.dynamic()
574            .await?
575            .call_typed("GetVotingItem", input)
576            .await
577    }
578
579    /// Returns a voting record by vote id hash.
580    pub async fn get_voting_record(
581        &self,
582        vote_id: &aelf_proto::aelf::Hash,
583    ) -> Result<vote::VotingRecord, ContractError> {
584        self.dynamic()
585            .await?
586            .call_typed("GetVotingRecord", vote_id)
587            .await
588    }
589
590    /// Returns the latest voting result for a voting item.
591    pub async fn get_latest_voting_result(
592        &self,
593        voting_item_id: &aelf_proto::aelf::Hash,
594    ) -> Result<vote::VotingResult, ContractError> {
595        self.dynamic()
596            .await?
597            .call_typed("GetLatestVotingResult", voting_item_id)
598            .await
599    }
600
601    async fn dynamic(&self) -> Result<DynamicContract, ContractError> {
602        self.dynamic.get().await
603    }
604}
605
606/// Typed wrapper for the cross-chain contract.
607#[derive(Clone)]
608pub struct CrossChainContract {
609    dynamic: LazyDynamicContract,
610}
611
612impl CrossChainContract {
613    /// Creates a typed cross-chain contract wrapper.
614    pub fn new(client: AElfClient, wallet: Wallet, address: impl Into<String>) -> Self {
615        Self {
616            dynamic: LazyDynamicContract::new(client, wallet, address),
617        }
618    }
619
620    /// Returns the parent chain id.
621    pub async fn get_parent_chain_id(&self) -> Result<i32, ContractError> {
622        let value: pbjson_types::Int32Value = self
623            .dynamic()
624            .await?
625            .call_typed("GetParentChainId", &pbjson_types::Empty {})
626            .await?;
627        Ok(value.value)
628    }
629
630    /// Returns the latest known parent chain height.
631    pub async fn get_parent_chain_height(&self) -> Result<i64, ContractError> {
632        let value: pbjson_types::Int64Value = self
633            .dynamic()
634            .await?
635            .call_typed("GetParentChainHeight", &pbjson_types::Empty {})
636            .await?;
637        Ok(value.value)
638    }
639
640    /// Returns the latest known side-chain height for the specified chain id.
641    pub async fn get_side_chain_height(&self, chain_id: i32) -> Result<i64, ContractError> {
642        let value: pbjson_types::Int64Value = self
643            .dynamic()
644            .await?
645            .call_typed(
646                "GetSideChainHeight",
647                &pbjson_types::Int32Value { value: chain_id },
648            )
649            .await?;
650        Ok(value.value)
651    }
652
653    /// Returns chain status information for the specified chain id.
654    pub async fn get_chain_status(
655        &self,
656        chain_id: i32,
657    ) -> Result<cross_chain::GetChainStatusOutput, ContractError> {
658        self.dynamic()
659            .await?
660            .call_typed(
661                "GetChainStatus",
662                &pbjson_types::Int32Value { value: chain_id },
663            )
664            .await
665    }
666
667    async fn dynamic(&self) -> Result<DynamicContract, ContractError> {
668        self.dynamic.get().await
669    }
670}
671
672/// Typed wrapper for the AEDPoS consensus contract.
673#[derive(Clone)]
674pub struct AedposContract {
675    dynamic: LazyDynamicContract,
676}
677
678impl AedposContract {
679    /// Creates a typed AEDPoS contract wrapper.
680    pub fn new(client: AElfClient, wallet: Wallet, address: impl Into<String>) -> Self {
681        Self {
682            dynamic: LazyDynamicContract::new(client, wallet, address),
683        }
684    }
685
686    /// Returns the current miner public keys as hex strings.
687    pub async fn get_current_miner_list(&self) -> Result<Vec<String>, ContractError> {
688        let output: aedpos::MinerList = self
689            .dynamic()
690            .await?
691            .call_typed("GetCurrentMinerList", &pbjson_types::Empty {})
692            .await?;
693        Ok(output.pubkeys.into_iter().map(hex::encode).collect())
694    }
695
696    /// Returns the current consensus round information.
697    pub async fn get_current_round_information(&self) -> Result<aedpos::Round, ContractError> {
698        self.dynamic()
699            .await?
700            .call_typed("GetCurrentRoundInformation", &pbjson_types::Empty {})
701            .await
702    }
703
704    async fn dynamic(&self) -> Result<DynamicContract, ContractError> {
705        self.dynamic.get().await
706    }
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712    use aelf_client::provider::Provider;
713    use async_trait::async_trait;
714    use base64::Engine;
715    use http::Method;
716    use serde_json::json;
717    use std::sync::{
718        atomic::{AtomicUsize, Ordering},
719        Arc,
720    };
721
722    const READONLY_PRIVATE_KEY: &str =
723        "0000000000000000000000000000000000000000000000000000000000000001";
724
725    fn token_method(name: &str) -> MethodDescriptor {
726        let pool = DescriptorPool::decode(aelf_proto::FILE_DESCRIPTOR_SET).expect("descriptor set");
727        let method = pool
728            .services()
729            .find_map(|service| service.methods().find(|method| method.name() == name))
730            .expect("token method");
731        method
732    }
733
734    #[test]
735    fn normalizes_address_strings_for_dynamic_input() {
736        let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY).expect("wallet");
737        let method = token_method("GetBalance");
738
739        let normalized = normalize_json_input(
740            &method.input(),
741            json!({
742                "symbol": "ELF",
743                "owner": wallet.address(),
744            }),
745        )
746        .expect("normalize input");
747
748        let message = DynamicMessage::deserialize(method.input(), normalized.into_deserializer())
749            .expect("deserialize");
750        let value = serde_json::to_value(message).expect("serialize");
751
752        assert_eq!(
753            value.get("owner"),
754            Some(
755                &serde_json::to_value(address_to_pb(wallet.address()).expect("address"))
756                    .expect("json")
757            ),
758        );
759    }
760
761    #[test]
762    fn normalizes_address_objects_for_dynamic_output() {
763        let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY).expect("wallet");
764        let method = token_method("GetBalance");
765        let output = token::GetBalanceOutput {
766            symbol: "ELF".to_owned(),
767            owner: Some(address_to_pb(wallet.address()).expect("address")),
768            balance: 42,
769        };
770
771        let normalized = normalize_json_output(
772            &method.output(),
773            serde_json::to_value(output).expect("json"),
774        )
775        .expect("normalize output");
776
777        assert_eq!(normalized.get("owner"), Some(&json!(wallet.address())));
778        assert_eq!(normalized.get("symbol"), Some(&json!("ELF")));
779    }
780
781    #[derive(Clone)]
782    struct CountingDescriptorProvider {
783        requests: Arc<AtomicUsize>,
784    }
785
786    #[async_trait]
787    impl Provider for CountingDescriptorProvider {
788        async fn request_json(
789            &self,
790            _method: Method,
791            _path: &str,
792            _query: &[(&str, String)],
793            _body: Option<Value>,
794        ) -> Result<Value, AElfError> {
795            Err(AElfError::request(
796                "unexpected JSON request in descriptor test",
797                None,
798            ))
799        }
800
801        async fn request_text(
802            &self,
803            method: Method,
804            path: &str,
805            query: &[(&str, String)],
806            _body: Option<Value>,
807        ) -> Result<String, AElfError> {
808            assert_eq!(method, Method::GET);
809            assert_eq!(path, "api/blockChain/contractFileDescriptorSet");
810            assert_eq!(query, &[("address", "token-contract".to_owned())]);
811
812            self.requests.fetch_add(1, Ordering::SeqCst);
813            Ok(format!(
814                "\"{}\"",
815                base64::engine::general_purpose::STANDARD.encode(aelf_proto::FILE_DESCRIPTOR_SET)
816            ))
817        }
818    }
819
820    #[tokio::test]
821    async fn dynamic_contract_fetches_descriptor_on_every_at_call() {
822        let requests = Arc::new(AtomicUsize::new(0));
823        let client = AElfClient::with_provider(CountingDescriptorProvider {
824            requests: requests.clone(),
825        })
826        .expect("client");
827        let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY).expect("wallet");
828
829        let first = DynamicContract::at(client.clone(), "token-contract", wallet.clone())
830            .await
831            .expect("first contract");
832        let second = DynamicContract::at(client, "token-contract", wallet)
833            .await
834            .expect("second contract");
835
836        assert!(first.method("GetBalance").is_ok());
837        assert!(second.method("GetBalance").is_ok());
838        assert_eq!(requests.load(Ordering::SeqCst), 2);
839    }
840
841    #[tokio::test]
842    async fn typed_wrapper_reuses_descriptor_within_same_handle() {
843        let requests = Arc::new(AtomicUsize::new(0));
844        let client = AElfClient::with_provider(CountingDescriptorProvider {
845            requests: requests.clone(),
846        })
847        .expect("client");
848        let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY).expect("wallet");
849        let token = TokenContract::new(client, wallet, "token-contract");
850
851        let first = token.dynamic().await.expect("first dynamic");
852        let second = token.dynamic().await.expect("second dynamic");
853
854        assert!(first.method("GetBalance").is_ok());
855        assert!(second.method("GetBalance").is_ok());
856        assert_eq!(requests.load(Ordering::SeqCst), 1);
857    }
858
859    #[tokio::test]
860    async fn typed_wrapper_clone_reuses_descriptor_cache() {
861        let requests = Arc::new(AtomicUsize::new(0));
862        let client = AElfClient::with_provider(CountingDescriptorProvider {
863            requests: requests.clone(),
864        })
865        .expect("client");
866        let wallet = Wallet::from_private_key(READONLY_PRIVATE_KEY).expect("wallet");
867        let first = TokenContract::new(client, wallet, "token-contract");
868        let second = first.clone();
869
870        assert!(first
871            .dynamic()
872            .await
873            .expect("first dynamic")
874            .method("GetBalance")
875            .is_ok());
876        assert!(second
877            .dynamic()
878            .await
879            .expect("second dynamic")
880            .method("GetBalance")
881            .is_ok());
882        assert_eq!(requests.load(Ordering::SeqCst), 1);
883    }
884}