1use crate::avatar::common::CommonAvatar;
2use crate::cid_v0_to_digest::cid_v0_to_digest;
3use crate::{
4 ContractRunner, Core, PreparedTransaction, Profile, SdkError, SubmittedTx, call_to_tx,
5};
6use alloy_primitives::{Address, Bytes, U256, aliases::U96};
7use circles_abis::BaseGroup;
8use circles_profiles::Profiles;
9#[cfg(feature = "ws")]
10use circles_rpc::events::subscription::CirclesSubscription;
11use circles_rpc::{CirclesRpc, PagedQuery};
12#[cfg(feature = "ws")]
13use circles_types::CirclesEvent;
14use circles_types::{
15 AdvancedTransferOptions, AggregatedTrustRelation, AvatarInfo, Balance, PathfindingResult,
16 SortOrder, TokenBalanceResponse, TransactionHistoryRow, TrustRelation,
17};
18use std::sync::Arc;
19
20pub struct BaseGroupAvatar {
22 pub address: Address,
24 pub info: AvatarInfo,
26 pub core: Arc<Core>,
28 pub runner: Option<Arc<dyn ContractRunner>>,
30 pub common: CommonAvatar,
32}
33
34impl BaseGroupAvatar {
35 pub async fn balances(
37 &self,
38 as_time_circles: bool,
39 use_v2: bool,
40 ) -> Result<Vec<TokenBalanceResponse>, SdkError> {
41 self.common.balances(as_time_circles, use_v2).await
42 }
43
44 pub async fn total_balance(
46 &self,
47 as_time_circles: bool,
48 use_v2: bool,
49 ) -> Result<Balance, SdkError> {
50 self.common.total_balance(as_time_circles, use_v2).await
51 }
52
53 pub async fn total_supply(&self) -> Result<U256, SdkError> {
55 let token_id = self
56 .core
57 .hub_v2()
58 .toTokenId(self.address)
59 .call()
60 .await
61 .map_err(|e| SdkError::Contract(e.to_string()))?;
62 self.core
63 .hub_v2()
64 .totalSupply(token_id)
65 .call()
66 .await
67 .map_err(|e| SdkError::Contract(e.to_string()))
68 }
69
70 pub async fn trust_relations(&self) -> Result<Vec<TrustRelation>, SdkError> {
72 self.common.trust_relations().await
73 }
74
75 pub async fn aggregated_trust_relations(
77 &self,
78 ) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
79 self.common.aggregated_trust_relations().await
80 }
81
82 pub async fn trusts(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
84 self.common.trusts().await
85 }
86
87 pub async fn trusted_by(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
89 self.common.trusted_by().await
90 }
91
92 pub async fn mutual_trusts(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
94 self.common.mutual_trusts().await
95 }
96
97 pub async fn is_trusting(&self, other_avatar: Address) -> Result<bool, SdkError> {
99 self.common.is_trusting(other_avatar).await
100 }
101
102 pub async fn is_trusted_by(&self, other_avatar: Address) -> Result<bool, SdkError> {
104 self.common.is_trusted_by(other_avatar).await
105 }
106
107 pub async fn owner(&self) -> Result<Address, SdkError> {
109 self.core
110 .base_group(self.address)
111 .owner()
112 .call()
113 .await
114 .map_err(|e| SdkError::Contract(e.to_string()))
115 }
116
117 pub async fn mint_handler(&self) -> Result<Address, SdkError> {
119 self.core
120 .base_group(self.address)
121 .BASE_MINT_HANDLER()
122 .call()
123 .await
124 .map_err(|e| SdkError::Contract(e.to_string()))
125 }
126
127 pub async fn service(&self) -> Result<Address, SdkError> {
129 self.core
130 .base_group(self.address)
131 .service()
132 .call()
133 .await
134 .map_err(|e| SdkError::Contract(e.to_string()))
135 }
136
137 pub async fn fee_collection(&self) -> Result<Address, SdkError> {
139 self.core
140 .base_group(self.address)
141 .feeCollection()
142 .call()
143 .await
144 .map_err(|e| SdkError::Contract(e.to_string()))
145 }
146
147 pub async fn membership_conditions(&self) -> Result<Vec<Address>, SdkError> {
149 self.core
150 .base_group(self.address)
151 .getMembershipConditions()
152 .call()
153 .await
154 .map_err(|e| SdkError::Contract(e.to_string()))
155 }
156
157 pub async fn profile(&self) -> Result<Option<Profile>, SdkError> {
159 self.common.profile(self.info.cid_v0.as_deref()).await
160 }
161
162 pub fn transaction_history(
164 &self,
165 limit: u32,
166 sort_order: SortOrder,
167 ) -> PagedQuery<TransactionHistoryRow> {
168 self.common.transaction_history(limit, sort_order)
169 }
170
171 pub async fn update_profile(&self, profile: &Profile) -> Result<Vec<SubmittedTx>, SdkError> {
173 let cid = self.common.pin_profile(profile).await?;
174 self.update_profile_metadata(&cid).await
175 }
176
177 pub async fn update_profile_metadata(&self, cid: &str) -> Result<Vec<SubmittedTx>, SdkError> {
179 let digest = cid_v0_to_digest(cid)?;
180 let call = circles_abis::BaseGroup::updateMetadataDigestCall {
181 _metadataDigest: digest,
182 };
183 let tx = call_to_tx(self.address, call, None);
184 self.common.send(vec![tx]).await
185 }
186
187 pub async fn register_short_name(&self, nonce: u64) -> Result<Vec<SubmittedTx>, SdkError> {
189 let call = circles_abis::BaseGroup::registerShortNameWithNonceCall {
190 _nonce: U256::from(nonce),
191 };
192 let tx = call_to_tx(self.address, call, None);
193 self.common.send(vec![tx]).await
194 }
195
196 pub async fn trust_add(
198 &self,
199 avatars: &[Address],
200 expiry: u128,
201 ) -> Result<Vec<SubmittedTx>, SdkError> {
202 let runner = self.runner.clone().ok_or(SdkError::MissingRunner)?;
203 let txs = avatars
204 .iter()
205 .map(|addr| BaseGroup::trustCall {
206 _trustReceiver: *addr,
207 _expiry: U96::from(expiry),
208 })
209 .map(|call| call_to_tx(self.address, call, None))
210 .collect();
211 Ok(runner.send_transactions(txs).await?)
212 }
213
214 pub async fn trust_remove(&self, avatars: &[Address]) -> Result<Vec<SubmittedTx>, SdkError> {
216 self.trust_add(avatars, 0).await
217 }
218
219 pub async fn trust_add_batch_with_conditions(
221 &self,
222 avatars: &[Address],
223 expiry: u128,
224 ) -> Result<Vec<SubmittedTx>, SdkError> {
225 let call = BaseGroup::trustBatchWithConditionsCall {
226 _members: avatars.to_vec(),
227 _expiry: U96::from(expiry),
228 };
229 let tx = call_to_tx(self.address, call, None);
230 self.common.send(vec![tx]).await
231 }
232
233 pub async fn set_owner(&self, owner: Address) -> Result<Vec<SubmittedTx>, SdkError> {
235 let call = BaseGroup::setOwnerCall { _owner: owner };
236 let tx = call_to_tx(self.address, call, None);
237 self.common.send(vec![tx]).await
238 }
239
240 pub async fn set_service(&self, service: Address) -> Result<Vec<SubmittedTx>, SdkError> {
242 let call = BaseGroup::setServiceCall { _service: service };
243 let tx = call_to_tx(self.address, call, None);
244 self.common.send(vec![tx]).await
245 }
246
247 pub async fn set_fee_collection(
249 &self,
250 fee_collection: Address,
251 ) -> Result<Vec<SubmittedTx>, SdkError> {
252 let call = BaseGroup::setFeeCollectionCall {
253 _feeCollection: fee_collection,
254 };
255 let tx = call_to_tx(self.address, call, None);
256 self.common.send(vec![tx]).await
257 }
258
259 pub async fn set_membership_condition(
261 &self,
262 condition: Address,
263 enabled: bool,
264 ) -> Result<Vec<SubmittedTx>, SdkError> {
265 let call = BaseGroup::setMembershipConditionCall {
266 _condition: condition,
267 _enabled: enabled,
268 };
269 let tx = call_to_tx(self.address, call, None);
270 self.common.send(vec![tx]).await
271 }
272
273 #[cfg(feature = "ws")]
274 pub async fn subscribe_events_ws(
275 &self,
276 ws_url: &str,
277 filter: Option<serde_json::Value>,
278 ) -> Result<CirclesSubscription<CirclesEvent>, SdkError> {
279 self.common.subscribe_events_ws(ws_url, filter).await
280 }
281
282 #[cfg(feature = "ws")]
283 pub async fn subscribe_events_ws_with_retries(
284 &self,
285 ws_url: &str,
286 filter: serde_json::Value,
287 max_attempts: Option<usize>,
288 ) -> Result<CirclesSubscription<CirclesEvent>, SdkError> {
289 self.common
290 .subscribe_events_ws_with_retries(ws_url, filter, max_attempts)
291 .await
292 }
293
294 pub async fn plan_transfer(
296 &self,
297 to: Address,
298 amount: U256,
299 options: Option<AdvancedTransferOptions>,
300 ) -> Result<Vec<PreparedTransaction>, SdkError> {
301 self.common.plan_transfer(to, amount, options).await
302 }
303
304 pub async fn transfer(
306 &self,
307 to: Address,
308 amount: U256,
309 options: Option<AdvancedTransferOptions>,
310 ) -> Result<Vec<SubmittedTx>, SdkError> {
311 self.common.transfer(to, amount, options).await
312 }
313
314 pub async fn plan_direct_transfer(
316 &self,
317 to: Address,
318 amount: U256,
319 token_address: Option<Address>,
320 tx_data: Option<Bytes>,
321 ) -> Result<Vec<PreparedTransaction>, SdkError> {
322 self.common
323 .plan_direct_transfer(to, amount, token_address, tx_data)
324 .await
325 }
326
327 pub async fn direct_transfer(
329 &self,
330 to: Address,
331 amount: U256,
332 token_address: Option<Address>,
333 tx_data: Option<Bytes>,
334 ) -> Result<Vec<SubmittedTx>, SdkError> {
335 self.common
336 .direct_transfer(to, amount, token_address, tx_data)
337 .await
338 }
339
340 pub async fn plan_replenish(
342 &self,
343 token_id: Address,
344 amount: U256,
345 receiver: Option<Address>,
346 ) -> Result<Vec<PreparedTransaction>, SdkError> {
347 self.common.plan_replenish(token_id, amount, receiver).await
348 }
349
350 pub async fn replenish(
352 &self,
353 token_id: Address,
354 amount: U256,
355 receiver: Option<Address>,
356 ) -> Result<Vec<SubmittedTx>, SdkError> {
357 self.common.replenish(token_id, amount, receiver).await
358 }
359
360 pub async fn find_path(
362 &self,
363 to: Address,
364 target_flow: U256,
365 options: Option<AdvancedTransferOptions>,
366 ) -> Result<PathfindingResult, SdkError> {
367 self.common.find_path(to, target_flow, options).await
368 }
369
370 pub async fn max_flow_to(
372 &self,
373 to: Address,
374 options: Option<AdvancedTransferOptions>,
375 ) -> Result<PathfindingResult, SdkError> {
376 self.common.max_flow_to(to, options).await
377 }
378
379 pub fn new(
381 address: Address,
382 info: AvatarInfo,
383 core: Arc<Core>,
384 profiles: Profiles,
385 rpc: Arc<CirclesRpc>,
386 runner: Option<Arc<dyn ContractRunner>>,
387 ) -> Self {
388 let common = CommonAvatar::new(address, core.clone(), profiles, rpc, runner.clone());
389 Self {
390 address,
391 info,
392 core,
393 runner,
394 common,
395 }
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use alloy_primitives::{Bytes, TxHash};
403 use alloy_sol_types::SolCall;
404 use async_trait::async_trait;
405 use circles_profiles::Profiles;
406 use circles_types::{AvatarType, CirclesConfig};
407 use std::sync::Mutex;
408
409 const TEST_CID: &str = "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG";
410
411 #[derive(Default)]
412 struct RecordingRunner {
413 sender: Address,
414 sent: Mutex<Vec<Vec<PreparedTransaction>>>,
415 }
416
417 #[async_trait]
418 impl ContractRunner for RecordingRunner {
419 fn sender_address(&self) -> Address {
420 self.sender
421 }
422
423 async fn send_transactions(
424 &self,
425 txs: Vec<PreparedTransaction>,
426 ) -> Result<Vec<crate::SubmittedTx>, crate::RunnerError> {
427 self.sent.lock().expect("lock").push(txs.clone());
428 Ok(txs
429 .into_iter()
430 .map(|_| crate::SubmittedTx {
431 tx_hash: Bytes::from(TxHash::ZERO.as_slice().to_vec()),
432 success: true,
433 index: None,
434 })
435 .collect())
436 }
437 }
438
439 fn dummy_config() -> CirclesConfig {
440 CirclesConfig {
441 circles_rpc_url: "https://rpc.example.com".into(),
442 pathfinder_url: "https://pathfinder.example.com".into(),
443 profile_service_url: "https://profiles.example.com".into(),
444 referrals_service_url: None,
445 v1_hub_address: Address::repeat_byte(0x01),
446 v2_hub_address: Address::repeat_byte(0x02),
447 name_registry_address: Address::repeat_byte(0x03),
448 base_group_mint_policy: Address::repeat_byte(0x04),
449 standard_treasury: Address::repeat_byte(0x05),
450 core_members_group_deployer: Address::repeat_byte(0x06),
451 base_group_factory_address: Address::repeat_byte(0x07),
452 lift_erc20_address: Address::repeat_byte(0x08),
453 invitation_escrow_address: Address::repeat_byte(0x09),
454 invitation_farm_address: Address::repeat_byte(0x0a),
455 referrals_module_address: Address::repeat_byte(0x0b),
456 invitation_module_address: Address::repeat_byte(0x0c),
457 }
458 }
459
460 fn dummy_avatar(address: Address) -> AvatarInfo {
461 AvatarInfo {
462 block_number: 0,
463 timestamp: None,
464 transaction_index: 0,
465 log_index: 0,
466 transaction_hash: TxHash::ZERO,
467 version: 2,
468 avatar_type: AvatarType::CrcV2RegisterGroup,
469 avatar: address,
470 token_id: None,
471 has_v1: false,
472 v1_token: None,
473 cid_v0_digest: None,
474 cid_v0: None,
475 v1_stopped: None,
476 is_human: false,
477 name: None,
478 symbol: None,
479 }
480 }
481
482 fn test_avatar() -> (BaseGroupAvatar, Arc<RecordingRunner>) {
483 let config = dummy_config();
484 let runner = Arc::new(RecordingRunner {
485 sender: Address::repeat_byte(0xcc),
486 sent: Mutex::new(Vec::new()),
487 });
488 let avatar = BaseGroupAvatar::new(
489 Address::repeat_byte(0xcc),
490 dummy_avatar(Address::repeat_byte(0xcc)),
491 Arc::new(Core::new(config.clone())),
492 Profiles::new(config.profile_service_url.clone()).expect("profiles"),
493 Arc::new(CirclesRpc::try_from_http(&config.circles_rpc_url).expect("rpc")),
494 Some(runner.clone()),
495 );
496 (avatar, runner)
497 }
498
499 #[tokio::test]
500 async fn base_group_write_helpers_encode_expected_calls() {
501 let (avatar, runner) = test_avatar();
502 let new_owner = Address::repeat_byte(0xdd);
503 let new_service = Address::repeat_byte(0xee);
504 let new_fee = Address::repeat_byte(0xff);
505 let condition = Address::repeat_byte(0x11);
506
507 avatar
508 .update_profile_metadata(TEST_CID)
509 .await
510 .expect("update metadata");
511 avatar
512 .register_short_name(5)
513 .await
514 .expect("register short name");
515 avatar
516 .trust_add_batch_with_conditions(&[new_owner, new_service], 42)
517 .await
518 .expect("trust batch");
519 avatar.set_owner(new_owner).await.expect("set owner");
520 avatar.set_service(new_service).await.expect("set service");
521 avatar
522 .set_fee_collection(new_fee)
523 .await
524 .expect("set fee collection");
525 avatar
526 .set_membership_condition(condition, true)
527 .await
528 .expect("set membership condition");
529
530 let sent = runner.sent.lock().expect("lock");
531 assert_eq!(sent.len(), 7);
532
533 assert_eq!(
534 &sent[0][0].data[..4],
535 &BaseGroup::updateMetadataDigestCall {
536 _metadataDigest: cid_v0_to_digest(TEST_CID).expect("cid"),
537 }
538 .abi_encode()[..4]
539 );
540 assert_eq!(
541 &sent[1][0].data[..4],
542 &BaseGroup::registerShortNameWithNonceCall {
543 _nonce: U256::from(5u64),
544 }
545 .abi_encode()[..4]
546 );
547 assert_eq!(
548 &sent[2][0].data[..4],
549 &BaseGroup::trustBatchWithConditionsCall {
550 _members: vec![new_owner, new_service],
551 _expiry: U96::from(42u128),
552 }
553 .abi_encode()[..4]
554 );
555 assert_eq!(
556 &sent[3][0].data[..4],
557 &BaseGroup::setOwnerCall { _owner: new_owner }.abi_encode()[..4]
558 );
559 assert_eq!(
560 &sent[4][0].data[..4],
561 &BaseGroup::setServiceCall {
562 _service: new_service,
563 }
564 .abi_encode()[..4]
565 );
566 assert_eq!(
567 &sent[5][0].data[..4],
568 &BaseGroup::setFeeCollectionCall {
569 _feeCollection: new_fee,
570 }
571 .abi_encode()[..4]
572 );
573 assert_eq!(
574 &sent[6][0].data[..4],
575 &BaseGroup::setMembershipConditionCall {
576 _condition: condition,
577 _enabled: true,
578 }
579 .abi_encode()[..4]
580 );
581 }
582}