1#![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#[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#[derive(Clone)]
45pub struct DynamicContract {
46 client: AElfClient,
47 address: String,
48 wallet: Wallet,
49 pool: DescriptorPool,
50}
51
52impl DynamicContract {
53 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 pub fn address(&self) -> &str {
76 &self.address
77 }
78
79 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 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 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 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 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#[derive(Clone)]
146pub struct DynamicContractMethod {
147 client: AElfClient,
148 address: String,
149 wallet: Wallet,
150 method: MethodDescriptor,
151}
152
153impl DynamicContractMethod {
154 pub fn descriptor(&self) -> &MethodDescriptor {
156 &self.method
157 }
158
159 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 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 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 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#[derive(Clone)]
379pub struct ZeroContract {
380 client: AElfClient,
381 wallet: Wallet,
382 address: String,
383}
384
385impl ZeroContract {
386 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 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#[derive(Clone)]
420pub struct TokenContract {
421 dynamic: LazyDynamicContract,
422}
423
424impl TokenContract {
425 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 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 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 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 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 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 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#[derive(Clone)]
495pub struct ElectionContract {
496 dynamic: LazyDynamicContract,
497}
498
499impl ElectionContract {
500 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 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 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 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#[derive(Clone)]
556pub struct VoteContract {
557 dynamic: LazyDynamicContract,
558}
559
560impl VoteContract {
561 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 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 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 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#[derive(Clone)]
608pub struct CrossChainContract {
609 dynamic: LazyDynamicContract,
610}
611
612impl CrossChainContract {
613 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 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 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 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 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#[derive(Clone)]
674pub struct AedposContract {
675 dynamic: LazyDynamicContract,
676}
677
678impl AedposContract {
679 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 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 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}