1use crate::avatar::common::CommonAvatar;
2use crate::cid_v0_to_digest::cid_v0_to_digest;
3use crate::runner::{PreparedTransaction as RunnerTx, SubmittedTx as RunnerSubmitted};
4use crate::services::referrals::{
5 ReferralPreviewList, ReferralPublicListOptions, Referrals, generate_private_key,
6 private_key_to_address,
7};
8use crate::{
9 ContractRunner, Core, PreparedTransaction, Profile, SdkError, SubmittedTx, call_to_tx,
10};
11use alloy_primitives::{Address, Bytes, U256, address, aliases::U96};
12use alloy_sol_types::{SolCall, SolValue, sol};
13use circles_abis::{HubV2, InvitationFarm, ReferralsModule};
14use circles_profiles::Profiles;
15#[cfg(feature = "ws")]
16use circles_rpc::events::subscription::CirclesSubscription;
17use circles_rpc::{CirclesRpc, PagedQuery};
18use circles_transfers::TransferBuilder;
19#[cfg(feature = "ws")]
20use circles_types::CirclesEvent;
21use circles_types::{
22 AdvancedTransferOptions, AggregatedTrustRelation, AllInvitationsResponse, AvatarInfo, Balance,
23 GroupMembershipRow, GroupQueryParams, GroupRow, InvitationOriginResponse,
24 InvitationsFromResponse, InvitedAccountInfo, PathfindingResult, PathfindingTransferStep,
25 SimulatedTrust, SortOrder, TokenBalanceResponse, TransactionHistoryRow, TrustRelation,
26};
27use std::collections::{BTreeMap, HashMap};
28use std::str::FromStr;
29use std::sync::Arc;
30
31pub struct HumanAvatar {
33 pub address: Address,
35 pub info: AvatarInfo,
37 pub core: Arc<Core>,
39 pub runner: Option<Arc<dyn ContractRunner>>,
41 pub common: CommonAvatar,
43}
44
45#[derive(Debug, Clone)]
47pub struct GeneratedInvites {
48 pub secrets: Vec<String>,
50 pub signers: Vec<Address>,
52 pub txs: Vec<RunnerTx>,
54 pub submitted: Option<Vec<RunnerSubmitted>>,
56}
57
58#[derive(Debug, Clone)]
60pub struct ReferralCodePlan {
61 pub private_key: String,
63 pub signer: Address,
65 pub txs: Vec<PreparedTransaction>,
67}
68
69#[derive(Debug, Clone, PartialEq, Eq)]
71pub struct ProxyInviter {
72 pub address: Address,
73 pub possible_invites: u64,
74}
75
76sol! {
77 struct ReferralPayload {
78 address referralsModule;
79 bytes callData;
80 }
81
82 #[sol(rpc)]
83 contract SafeMinimal {
84 function isModuleEnabled(address module) external view returns (bool);
85 function enableModule(address module) external;
86 }
87
88 #[sol(rpc)]
89 contract InvitationModuleMinimal {
90 function trustInviter(address inviter) external;
91 }
92}
93
94fn invitation_fee_amount() -> U256 {
95 U256::from(96u128) * U256::from(10).pow(U256::from(18))
96}
97
98fn invitation_max_flow() -> U256 {
99 U256::from_str("9999999999999999999999999999999999999").expect("valid invitation max flow")
100}
101
102fn gnosis_group_address() -> Address {
103 address!("c19bc204eb1c1d5b3fe500e5e5dfabab625f286c")
104}
105
106fn farm_destination_address() -> Address {
107 address!("9Eb51E6A39B3F17bB1883B80748b56170039ff1d")
108}
109
110fn farm_quota_holder() -> Address {
111 address!("20EcD8bDeb2F48d8a7c94E542aA4feC5790D9676")
112}
113
114fn encode_direct_invite_data(invitee: Address) -> Bytes {
115 Bytes::from(invitee.abi_encode())
116}
117
118fn build_enable_module_tx(inviter: Address, invitation_module: Address) -> PreparedTransaction {
119 let call = SafeMinimal::enableModuleCall {
120 module: invitation_module,
121 };
122 call_to_tx(inviter, call, None)
123}
124
125fn build_trust_inviter_tx(invitation_module: Address, inviter: Address) -> PreparedTransaction {
126 let call = InvitationModuleMinimal::trustInviterCall { inviter };
127 call_to_tx(invitation_module, call, None)
128}
129
130fn build_inviter_setup_txs(
131 inviter: Address,
132 invitation_module: Address,
133 module_enabled: bool,
134 inviter_trusted: bool,
135) -> Vec<PreparedTransaction> {
136 let mut txs = Vec::new();
137
138 if !module_enabled {
139 txs.push(build_enable_module_tx(inviter, invitation_module));
140 txs.push(build_trust_inviter_tx(invitation_module, inviter));
141 } else if !inviter_trusted {
142 txs.push(build_trust_inviter_tx(invitation_module, inviter));
143 }
144
145 txs
146}
147
148fn transfer_txs_to_prepared(txs: Vec<circles_transfers::TransferTx>) -> Vec<PreparedTransaction> {
149 txs.into_iter()
150 .map(|tx| PreparedTransaction {
151 to: tx.to,
152 data: tx.data,
153 value: Some(tx.value),
154 })
155 .collect()
156}
157
158fn build_claim_invite_tx(invitation_farm: Address) -> PreparedTransaction {
159 let call = InvitationFarm::claimInviteCall {};
160 call_to_tx(invitation_farm, call, None)
161}
162
163fn encode_referral_invite_data(signer: Address, referrals_module: Address) -> Bytes {
164 let create_account = ReferralsModule::createAccountCall { signer };
165 let payload = ReferralPayload {
166 referralsModule: referrals_module,
167 callData: create_account.abi_encode().into(),
168 };
169 Bytes::from(payload.abi_encode())
170}
171
172fn build_invitation_transfer_tx(
173 hub: Address,
174 from: Address,
175 invitation_module: Address,
176 claimed_id: U256,
177 tx_data: Bytes,
178) -> PreparedTransaction {
179 let call = HubV2::safeTransferFromCall {
180 _from: from,
181 _to: invitation_module,
182 _id: claimed_id,
183 _value: invitation_fee_amount(),
184 _data: tx_data,
185 };
186 call_to_tx(hub, call, None)
187}
188
189fn order_proxy_inviters(mut inviters: Vec<ProxyInviter>, inviter: Address) -> Vec<ProxyInviter> {
190 inviters.sort_by(|a, b| {
191 let a_is_inviter = a.address == inviter;
192 let b_is_inviter = b.address == inviter;
193 b_is_inviter
194 .cmp(&a_is_inviter)
195 .then_with(|| format!("{:#x}", a.address).cmp(&format!("{:#x}", b.address)))
196 });
197 inviters
198}
199
200fn summarize_proxy_inviters(
201 terminal_transfers: &[PathfindingTransferStep],
202 owner_remap: &HashMap<Address, Address>,
203 inviter: Address,
204) -> Vec<ProxyInviter> {
205 let mut totals = BTreeMap::<Address, U256>::new();
206
207 for transfer in terminal_transfers {
208 let Ok(raw_owner) = Address::from_str(&transfer.token_owner) else {
209 continue;
210 };
211 let resolved_owner = owner_remap.get(&raw_owner).copied().unwrap_or(raw_owner);
212 totals
213 .entry(resolved_owner)
214 .and_modify(|total| *total += transfer.value)
215 .or_insert(transfer.value);
216 }
217
218 let fee = invitation_fee_amount();
219 let inviters = totals
220 .into_iter()
221 .filter_map(|(address, amount)| {
222 let possible = amount / fee;
223 if possible == U256::ZERO {
224 return None;
225 }
226 Some(ProxyInviter {
227 address,
228 possible_invites: u64::try_from(possible).unwrap_or(u64::MAX),
229 })
230 })
231 .collect::<Vec<_>>();
232
233 order_proxy_inviters(inviters, inviter)
234}
235
236impl HumanAvatar {
237 async fn ensure_inviter_setup(&self) -> Result<Vec<PreparedTransaction>, SdkError> {
238 let invitation_module = self.common.core.config.invitation_module_address;
239 let module_enabled = SafeMinimal::new(self.address, self.core.provider())
240 .isModuleEnabled(invitation_module)
241 .call()
242 .await
243 .map_err(|e| SdkError::Contract(e.to_string()))?;
244
245 let inviter_trusted = if module_enabled {
246 self.core
247 .hub_v2()
248 .isTrusted(invitation_module, self.address)
249 .call()
250 .await
251 .map_err(|e| SdkError::Contract(e.to_string()))?
252 } else {
253 false
254 };
255
256 Ok(build_inviter_setup_txs(
257 self.address,
258 invitation_module,
259 module_enabled,
260 inviter_trusted,
261 ))
262 }
263
264 async fn plan_invitation_delivery(
265 &self,
266 tx_data: Bytes,
267 ) -> Result<Vec<PreparedTransaction>, SdkError> {
268 let invitation_module = self.common.core.config.invitation_module_address;
269 let mut transactions = self.ensure_inviter_setup().await?;
270 let transfer_builder = TransferBuilder::new(self.common.core.config.clone())?;
271 let proxy_inviters = self.proxy_inviters().await?;
272
273 if let Some(proxy_inviter) = proxy_inviters.first() {
274 let transfer_txs = transfer_builder
275 .construct_advanced_transfer_with_aggregate(
276 self.address,
277 invitation_module,
278 invitation_fee_amount(),
279 Some(AdvancedTransferOptions {
280 use_wrapped_balances: Some(true),
281 from_tokens: None,
282 to_tokens: Some(vec![proxy_inviter.address]),
283 exclude_from_tokens: None,
284 exclude_to_tokens: None,
285 simulated_balances: None,
286 simulated_trusts: Some(vec![SimulatedTrust {
287 truster: invitation_module,
288 trustee: self.address,
289 }]),
290 max_transfers: None,
291 tx_data: Some(tx_data.clone()),
292 }),
293 true,
294 )
295 .await?;
296 transactions.extend(transfer_txs_to_prepared(transfer_txs));
297 return Ok(transactions);
298 }
299
300 let farm_txs = transfer_builder
301 .construct_advanced_transfer_with_aggregate(
302 self.address,
303 farm_destination_address(),
304 invitation_fee_amount(),
305 Some(AdvancedTransferOptions {
306 use_wrapped_balances: Some(true),
307 from_tokens: None,
308 to_tokens: Some(vec![gnosis_group_address()]),
309 exclude_from_tokens: None,
310 exclude_to_tokens: None,
311 simulated_balances: None,
312 simulated_trusts: None,
313 max_transfers: None,
314 tx_data: None,
315 }),
316 true,
317 )
318 .await?;
319 transactions.extend(transfer_txs_to_prepared(farm_txs));
320
321 let claimed_id = self
322 .core
323 .invitation_farm()
324 .claimInvite()
325 .from(farm_quota_holder())
326 .call()
327 .await
328 .map_err(|e| SdkError::Contract(e.to_string()))?;
329 let live_invitation_module = self.invitation_module().await?;
330
331 transactions.push(build_claim_invite_tx(
332 self.common.core.config.invitation_farm_address,
333 ));
334 transactions.push(build_invitation_transfer_tx(
335 self.common.core.config.v2_hub_address,
336 self.address,
337 live_invitation_module,
338 claimed_id,
339 tx_data,
340 ));
341
342 Ok(transactions)
343 }
344
345 pub async fn balances(
347 &self,
348 as_time_circles: bool,
349 use_v2: bool,
350 ) -> Result<Vec<TokenBalanceResponse>, SdkError> {
351 self.common.balances(as_time_circles, use_v2).await
352 }
353
354 pub async fn total_balance(
356 &self,
357 as_time_circles: bool,
358 use_v2: bool,
359 ) -> Result<Balance, SdkError> {
360 self.common.total_balance(as_time_circles, use_v2).await
361 }
362
363 pub async fn trust_relations(&self) -> Result<Vec<TrustRelation>, SdkError> {
365 self.common.trust_relations().await
366 }
367
368 pub async fn aggregated_trust_relations(
370 &self,
371 ) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
372 self.common.aggregated_trust_relations().await
373 }
374
375 pub async fn trusts(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
377 self.common.trusts().await
378 }
379
380 pub async fn trusted_by(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
382 self.common.trusted_by().await
383 }
384
385 pub async fn mutual_trusts(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
387 self.common.mutual_trusts().await
388 }
389
390 pub async fn is_trusting(&self, other_avatar: Address) -> Result<bool, SdkError> {
392 self.common.is_trusting(other_avatar).await
393 }
394
395 pub async fn is_trusted_by(&self, other_avatar: Address) -> Result<bool, SdkError> {
397 self.common.is_trusted_by(other_avatar).await
398 }
399
400 pub async fn profile(&self) -> Result<Option<Profile>, SdkError> {
402 self.common.profile(self.info.cid_v0.as_deref()).await
403 }
404
405 pub fn transaction_history(
407 &self,
408 limit: u32,
409 sort_order: SortOrder,
410 ) -> PagedQuery<TransactionHistoryRow> {
411 self.common.transaction_history(limit, sort_order)
412 }
413
414 pub async fn update_profile(&self, profile: &Profile) -> Result<Vec<SubmittedTx>, SdkError> {
416 let cid = self.common.pin_profile(profile).await?;
417 self.update_profile_metadata(&cid).await
418 }
419
420 pub async fn update_profile_metadata(&self, cid: &str) -> Result<Vec<SubmittedTx>, SdkError> {
422 let digest = cid_v0_to_digest(cid)?;
423 let call = circles_abis::NameRegistry::updateMetadataDigestCall {
424 _metadataDigest: digest,
425 };
426 let tx = call_to_tx(self.core.config.name_registry_address, call, None);
427 self.common.send(vec![tx]).await
428 }
429
430 pub async fn register_short_name(&self, nonce: u64) -> Result<Vec<SubmittedTx>, SdkError> {
432 let call = circles_abis::NameRegistry::registerShortNameWithNonceCall {
433 _nonce: U256::from(nonce),
434 };
435 let tx = call_to_tx(self.core.config.name_registry_address, call, None);
436 self.common.send(vec![tx]).await
437 }
438
439 pub async fn trust_add(
441 &self,
442 avatars: &[Address],
443 expiry: u128,
444 ) -> Result<Vec<SubmittedTx>, SdkError> {
445 let runner = self.runner.clone().ok_or(SdkError::MissingRunner)?;
446 let txs = avatars
447 .iter()
448 .map(|addr| HubV2::trustCall {
449 _trustReceiver: *addr,
450 _expiry: U96::from(expiry),
451 })
452 .map(|call| call_to_tx(self.core.config.v2_hub_address, call, None))
453 .collect();
454 Ok(runner.send_transactions(txs).await?)
455 }
456
457 pub async fn trust_remove(&self, avatars: &[Address]) -> Result<Vec<SubmittedTx>, SdkError> {
459 self.trust_add(avatars, 0).await
460 }
461
462 #[cfg(feature = "ws")]
463 pub async fn subscribe_events_ws(
464 &self,
465 ws_url: &str,
466 filter: Option<serde_json::Value>,
467 ) -> Result<CirclesSubscription<CirclesEvent>, SdkError> {
468 self.common.subscribe_events_ws(ws_url, filter).await
469 }
470
471 #[cfg(feature = "ws")]
472 pub async fn subscribe_events_ws_with_retries(
473 &self,
474 ws_url: &str,
475 filter: serde_json::Value,
476 max_attempts: Option<usize>,
477 ) -> Result<CirclesSubscription<CirclesEvent>, SdkError> {
478 self.common
479 .subscribe_events_ws_with_retries(ws_url, filter, max_attempts)
480 .await
481 }
482
483 #[cfg(feature = "ws")]
484 pub async fn subscribe_events_ws_with_catchup(
485 &self,
486 ws_url: &str,
487 filter: serde_json::Value,
488 max_attempts: Option<usize>,
489 catch_up_from_block: Option<u64>,
490 catch_up_filter: Option<Vec<circles_types::Filter>>,
491 ) -> Result<(Vec<CirclesEvent>, CirclesSubscription<CirclesEvent>), SdkError> {
492 self.common
493 .subscribe_events_ws_with_catchup(
494 ws_url,
495 filter,
496 max_attempts,
497 catch_up_from_block,
498 catch_up_filter,
499 )
500 .await
501 }
502
503 pub async fn plan_transfer(
505 &self,
506 to: Address,
507 amount: U256,
508 options: Option<AdvancedTransferOptions>,
509 ) -> Result<Vec<PreparedTransaction>, SdkError> {
510 self.common.plan_transfer(to, amount, options).await
511 }
512
513 pub async fn transfer(
515 &self,
516 to: Address,
517 amount: U256,
518 options: Option<AdvancedTransferOptions>,
519 ) -> Result<Vec<SubmittedTx>, SdkError> {
520 self.common.transfer(to, amount, options).await
521 }
522
523 pub async fn plan_direct_transfer(
525 &self,
526 to: Address,
527 amount: U256,
528 token_address: Option<Address>,
529 tx_data: Option<Bytes>,
530 ) -> Result<Vec<PreparedTransaction>, SdkError> {
531 self.common
532 .plan_direct_transfer(to, amount, token_address, tx_data)
533 .await
534 }
535
536 pub async fn direct_transfer(
538 &self,
539 to: Address,
540 amount: U256,
541 token_address: Option<Address>,
542 tx_data: Option<Bytes>,
543 ) -> Result<Vec<SubmittedTx>, SdkError> {
544 self.common
545 .direct_transfer(to, amount, token_address, tx_data)
546 .await
547 }
548
549 pub async fn plan_replenish(
551 &self,
552 token_id: Address,
553 amount: U256,
554 receiver: Option<Address>,
555 ) -> Result<Vec<PreparedTransaction>, SdkError> {
556 self.common.plan_replenish(token_id, amount, receiver).await
557 }
558
559 pub async fn replenish(
561 &self,
562 token_id: Address,
563 amount: U256,
564 receiver: Option<Address>,
565 ) -> Result<Vec<SubmittedTx>, SdkError> {
566 self.common.replenish(token_id, amount, receiver).await
567 }
568
569 pub async fn max_replenishable(
571 &self,
572 options: Option<AdvancedTransferOptions>,
573 ) -> Result<U256, SdkError> {
574 let mut opts = options.unwrap_or(AdvancedTransferOptions {
575 use_wrapped_balances: None,
576 from_tokens: None,
577 to_tokens: None,
578 exclude_from_tokens: None,
579 exclude_to_tokens: None,
580 simulated_balances: None,
581 simulated_trusts: None,
582 max_transfers: None,
583 tx_data: None,
584 });
585 if opts.use_wrapped_balances.is_none() {
586 opts.use_wrapped_balances = Some(true);
587 }
588 if opts.to_tokens.is_none() {
589 opts.to_tokens = Some(vec![self.address]);
590 }
591 Ok(self
592 .common
593 .find_path(self.address, U256::MAX, Some(opts))
594 .await?
595 .max_flow)
596 }
597
598 pub async fn plan_replenish_max(
600 &self,
601 options: Option<AdvancedTransferOptions>,
602 ) -> Result<Vec<PreparedTransaction>, SdkError> {
603 let max_amount = self.max_replenishable(options).await?;
604 if max_amount.is_zero() {
605 return Err(SdkError::OperationFailed(
606 "no tokens available to replenish".to_string(),
607 ));
608 }
609 self.plan_replenish(self.address, max_amount, None).await
610 }
611
612 pub async fn replenish_max(
614 &self,
615 options: Option<AdvancedTransferOptions>,
616 ) -> Result<Vec<SubmittedTx>, SdkError> {
617 let txs = self.plan_replenish_max(options).await?;
618 self.common.send(txs).await
619 }
620
621 pub async fn plan_group_token_mint(
623 &self,
624 group: Address,
625 amount: U256,
626 ) -> Result<Vec<PreparedTransaction>, SdkError> {
627 let mint_handler = self
628 .core
629 .base_group(group)
630 .BASE_MINT_HANDLER()
631 .call()
632 .await
633 .map_err(|e| SdkError::Contract(e.to_string()))?;
634 self.plan_transfer(
635 mint_handler,
636 amount,
637 Some(AdvancedTransferOptions {
638 use_wrapped_balances: Some(true),
639 from_tokens: None,
640 to_tokens: None,
641 exclude_from_tokens: None,
642 exclude_to_tokens: None,
643 simulated_balances: None,
644 simulated_trusts: None,
645 max_transfers: None,
646 tx_data: None,
647 }),
648 )
649 .await
650 }
651
652 pub async fn mint_group_token(
654 &self,
655 group: Address,
656 amount: U256,
657 ) -> Result<Vec<SubmittedTx>, SdkError> {
658 let txs = self.plan_group_token_mint(group, amount).await?;
659 self.common.send(txs).await
660 }
661
662 pub async fn plan_group_token_redeem(
664 &self,
665 group: Address,
666 amount: U256,
667 ) -> Result<Vec<PreparedTransaction>, SdkError> {
668 self.common.plan_group_token_redeem(group, amount).await
669 }
670
671 pub async fn redeem_group_token(
673 &self,
674 group: Address,
675 amount: U256,
676 ) -> Result<Vec<SubmittedTx>, SdkError> {
677 self.common.group_token_redeem(group, amount).await
678 }
679
680 pub async fn max_group_token_mintable(&self, group: Address) -> Result<U256, SdkError> {
682 let mint_handler = self
683 .core
684 .base_group(group)
685 .BASE_MINT_HANDLER()
686 .call()
687 .await
688 .map_err(|e| SdkError::Contract(e.to_string()))?;
689 Ok(self
690 .max_flow_to(
691 mint_handler,
692 Some(AdvancedTransferOptions {
693 use_wrapped_balances: Some(true),
694 from_tokens: None,
695 to_tokens: None,
696 exclude_from_tokens: None,
697 exclude_to_tokens: None,
698 simulated_balances: None,
699 simulated_trusts: None,
700 max_transfers: None,
701 tx_data: None,
702 }),
703 )
704 .await?
705 .max_flow)
706 }
707
708 pub async fn find_path(
710 &self,
711 to: Address,
712 target_flow: U256,
713 options: Option<AdvancedTransferOptions>,
714 ) -> Result<PathfindingResult, SdkError> {
715 self.common.find_path(to, target_flow, options).await
716 }
717
718 pub async fn max_flow_to(
720 &self,
721 to: Address,
722 options: Option<AdvancedTransferOptions>,
723 ) -> Result<PathfindingResult, SdkError> {
724 self.common.max_flow_to(to, options).await
725 }
726
727 pub async fn group_owner(&self, group: Address) -> Result<Address, SdkError> {
729 self.core
730 .base_group(group)
731 .owner()
732 .call()
733 .await
734 .map_err(|e| SdkError::Contract(e.to_string()))
735 }
736
737 pub async fn group_mint_handler(&self, group: Address) -> Result<Address, SdkError> {
739 self.core
740 .base_group(group)
741 .BASE_MINT_HANDLER()
742 .call()
743 .await
744 .map_err(|e| SdkError::Contract(e.to_string()))
745 }
746
747 pub async fn group_treasury(&self, group: Address) -> Result<Address, SdkError> {
749 self.core
750 .base_group(group)
751 .BASE_TREASURY()
752 .call()
753 .await
754 .map_err(|e| SdkError::Contract(e.to_string()))
755 }
756
757 pub async fn group_service(&self, group: Address) -> Result<Address, SdkError> {
759 self.core
760 .base_group(group)
761 .service()
762 .call()
763 .await
764 .map_err(|e| SdkError::Contract(e.to_string()))
765 }
766
767 pub async fn group_fee_collection(&self, group: Address) -> Result<Address, SdkError> {
769 self.core
770 .base_group(group)
771 .feeCollection()
772 .call()
773 .await
774 .map_err(|e| SdkError::Contract(e.to_string()))
775 }
776
777 pub async fn group_membership_conditions(
779 &self,
780 group: Address,
781 ) -> Result<Vec<Address>, SdkError> {
782 self.core
783 .base_group(group)
784 .getMembershipConditions()
785 .call()
786 .await
787 .map_err(|e| SdkError::Contract(e.to_string()))
788 }
789
790 pub fn group_memberships(
792 &self,
793 limit: u32,
794 sort_order: SortOrder,
795 ) -> PagedQuery<GroupMembershipRow> {
796 self.common
797 .rpc
798 .group()
799 .get_group_memberships(self.address, limit, sort_order)
800 }
801
802 pub async fn group_membership_details(&self, limit: u32) -> Result<Vec<GroupRow>, SdkError> {
807 let mut query = self.group_memberships(limit, SortOrder::DESC);
808 let mut memberships = Vec::new();
809
810 while let Some(page) = query.next_page().await? {
811 memberships.extend(page.items);
812 if !page.has_more {
813 break;
814 }
815 }
816
817 if memberships.is_empty() {
818 return Ok(Vec::new());
819 }
820
821 let group_addresses = memberships
822 .into_iter()
823 .map(|membership| membership.group)
824 .collect::<Vec<_>>();
825
826 Ok(self
827 .common
828 .rpc
829 .group()
830 .find_groups(
831 group_addresses.len() as u32,
832 Some(GroupQueryParams {
833 group_address_in: Some(group_addresses),
834 ..Default::default()
835 }),
836 )
837 .await?)
838 }
839
840 pub async fn personal_mint(&self) -> Result<Vec<SubmittedTx>, SdkError> {
842 let call = HubV2::personalMintCall {};
843 let tx = call_to_tx(self.core.config.v2_hub_address, call, None);
844 self.common.send(vec![tx]).await
845 }
846
847 pub async fn stop_mint(&self) -> Result<Vec<SubmittedTx>, SdkError> {
849 let call = HubV2::stopCall {};
850 let tx = call_to_tx(self.core.config.v2_hub_address, call, None);
851 self.common.send(vec![tx]).await
852 }
853
854 async fn generate_invites_inner(
855 &self,
856 number_of_invites: u64,
857 ) -> Result<GeneratedInvites, SdkError> {
858 if number_of_invites == 0 {
859 return Err(SdkError::InvalidRegistration(
860 "number_of_invites must be greater than 0".to_string(),
861 ));
862 }
863
864 let claim_call = InvitationFarm::claimInvitesCall {
866 numberOfInvites: U256::from(number_of_invites),
867 };
868 let ids = self
869 .common
870 .core
871 .invitation_farm()
872 .claimInvites(U256::from(number_of_invites))
873 .call()
874 .await
875 .unwrap_or_default();
876 if ids.is_empty() {
877 return Err(SdkError::InvalidRegistration(
878 "invitation farm returned no ids".to_string(),
879 ));
880 }
881
882 let mut secrets = Vec::with_capacity(ids.len());
884 let mut signers = Vec::with_capacity(ids.len());
885 for _ in &ids {
886 let secret = generate_private_key();
887 let signer = private_key_to_address(&secret)?;
888 secrets.push(secret);
889 signers.push(signer);
890 }
891
892 let create_accounts = ReferralsModule::createAccountsCall {
894 signers: signers.clone(),
895 };
896 let referrals_module = self.common.core.config.referrals_module_address;
897 let payload = ReferralPayload {
898 referralsModule: referrals_module,
899 callData: create_accounts.abi_encode().into(),
900 };
901 let encoded_payload = payload.abi_encode();
902
903 let amount = invitation_fee_amount();
905 let values = vec![amount; ids.len()];
906
907 let invitation_module = self
909 .common
910 .core
911 .invitation_farm()
912 .invitationModule()
913 .call()
914 .await
915 .unwrap_or_default();
916
917 let claim_tx = call_to_tx(
918 self.common.core.config.invitation_farm_address,
919 claim_call,
920 None,
921 );
922 let batch_call = HubV2::safeBatchTransferFromCall {
923 _from: self.address,
924 _to: invitation_module,
925 _ids: ids,
926 _values: values,
927 _data: encoded_payload.into(),
928 };
929 let batch_tx = call_to_tx(self.common.core.config.v2_hub_address, batch_call, None);
930
931 Ok(GeneratedInvites {
932 secrets,
933 signers,
934 txs: vec![
935 RunnerTx {
936 to: claim_tx.to,
937 data: claim_tx.data,
938 value: claim_tx.value,
939 },
940 RunnerTx {
941 to: batch_tx.to,
942 data: batch_tx.data,
943 value: batch_tx.value,
944 },
945 ],
946 submitted: None,
947 })
948 }
949
950 pub async fn plan_generate_referrals(
952 &self,
953 number_of_invites: u64,
954 ) -> Result<GeneratedInvites, SdkError> {
955 self.generate_invites_inner(number_of_invites).await
956 }
957
958 pub async fn generate_invites(
960 &self,
961 number_of_invites: u64,
962 ) -> Result<GeneratedInvites, SdkError> {
963 self.plan_generate_referrals(number_of_invites).await
964 }
965
966 pub async fn generate_referrals(
968 &self,
969 number_of_invites: u64,
970 ) -> Result<GeneratedInvites, SdkError> {
971 let generated = self.plan_generate_referrals(number_of_invites).await?;
972 self.submit_generated_referrals(generated).await
973 }
974
975 pub fn invitation_fee(&self) -> U256 {
977 invitation_fee_amount()
978 }
979
980 pub async fn invitation_module(&self) -> Result<Address, SdkError> {
982 self.common
983 .core
984 .invitation_farm()
985 .invitationModule()
986 .call()
987 .await
988 .map_err(|e| SdkError::Contract(e.to_string()))
989 }
990
991 pub async fn invitation_quota(&self) -> Result<U256, SdkError> {
993 self.common
994 .core
995 .invitation_farm()
996 .inviterQuota(self.address)
997 .call()
998 .await
999 .map_err(|e| SdkError::Contract(e.to_string()))
1000 }
1001
1002 pub async fn compute_referral_address(&self, signer: Address) -> Result<Address, SdkError> {
1004 self.common
1005 .core
1006 .referrals_module()
1007 .computeAddress(signer)
1008 .call()
1009 .await
1010 .map_err(|e| SdkError::Contract(e.to_string()))
1011 }
1012
1013 pub async fn invitation_origin(&self) -> Result<Option<InvitationOriginResponse>, SdkError> {
1015 Ok(self
1016 .common
1017 .rpc
1018 .invitation()
1019 .get_invitation_origin(self.address)
1020 .await?)
1021 }
1022
1023 pub async fn invited_by(&self) -> Result<Option<Address>, SdkError> {
1025 Ok(self
1026 .common
1027 .rpc
1028 .invitation()
1029 .get_invited_by(self.address)
1030 .await?)
1031 }
1032
1033 pub async fn available_invitations(
1035 &self,
1036 minimum_balance: Option<String>,
1037 ) -> Result<AllInvitationsResponse, SdkError> {
1038 Ok(self
1039 .common
1040 .rpc
1041 .invitation()
1042 .get_all_invitations(self.address, minimum_balance)
1043 .await?)
1044 }
1045
1046 pub async fn proxy_inviters(&self) -> Result<Vec<ProxyInviter>, SdkError> {
1048 let invitation_module = self.common.core.config.invitation_module_address;
1049
1050 let gnosis_group_trusts = self
1051 .common
1052 .rpc
1053 .trust()
1054 .get_trusts(gnosis_group_address())
1055 .await?;
1056 let trusts_inviter_relations = self.common.rpc.trust().get_trusted_by(self.address).await?;
1057 let mutual_trust_relations = self
1058 .common
1059 .rpc
1060 .trust()
1061 .get_mutual_trusts(self.address)
1062 .await?;
1063 let module_trusts_relations = self
1064 .common
1065 .rpc
1066 .trust()
1067 .get_trusts(invitation_module)
1068 .await?;
1069 let module_mutual_trust_relations = self
1070 .common
1071 .rpc
1072 .trust()
1073 .get_mutual_trusts(invitation_module)
1074 .await?;
1075
1076 let set1 = gnosis_group_trusts
1077 .into_iter()
1078 .map(|relation| relation.object_avatar)
1079 .collect::<std::collections::HashSet<_>>();
1080 let set2 = trusts_inviter_relations
1081 .into_iter()
1082 .chain(mutual_trust_relations.into_iter())
1083 .map(|relation| relation.object_avatar)
1084 .collect::<std::collections::HashSet<_>>();
1085 let set3 = module_trusts_relations
1086 .into_iter()
1087 .chain(module_mutual_trust_relations.into_iter())
1088 .map(|relation| relation.object_avatar)
1089 .collect::<std::collections::HashSet<_>>();
1090
1091 let mut tokens_to_use = set2
1092 .into_iter()
1093 .filter(|address| set3.contains(address) && !set1.contains(address))
1094 .collect::<Vec<_>>();
1095 tokens_to_use.push(self.address);
1096
1097 let path = self
1098 .common
1099 .find_path(
1100 invitation_module,
1101 invitation_max_flow(),
1102 Some(AdvancedTransferOptions {
1103 use_wrapped_balances: Some(true),
1104 from_tokens: None,
1105 to_tokens: Some(tokens_to_use),
1106 exclude_from_tokens: None,
1107 exclude_to_tokens: None,
1108 simulated_balances: None,
1109 simulated_trusts: Some(vec![SimulatedTrust {
1110 truster: invitation_module,
1111 trustee: self.address,
1112 }]),
1113 max_transfers: None,
1114 tx_data: None,
1115 }),
1116 )
1117 .await?;
1118
1119 if path.transfers.is_empty() {
1120 return Ok(Vec::new());
1121 }
1122
1123 let terminal_transfers = path
1124 .transfers
1125 .iter()
1126 .filter(|transfer| transfer.to == invitation_module)
1127 .cloned()
1128 .collect::<Vec<_>>();
1129 if terminal_transfers.is_empty() {
1130 return Ok(Vec::new());
1131 }
1132
1133 let raw_owners = terminal_transfers
1134 .iter()
1135 .filter_map(|transfer| Address::from_str(&transfer.token_owner).ok())
1136 .collect::<std::collections::BTreeSet<_>>()
1137 .into_iter()
1138 .collect::<Vec<_>>();
1139
1140 let token_infos = self
1141 .common
1142 .rpc
1143 .token_info()
1144 .get_token_info_batch(raw_owners)
1145 .await?;
1146 let owner_remap = token_infos
1147 .into_iter()
1148 .map(|info| (info.token, info.token_owner))
1149 .collect::<HashMap<_, _>>();
1150
1151 Ok(summarize_proxy_inviters(
1152 &terminal_transfers,
1153 &owner_remap,
1154 self.address,
1155 ))
1156 }
1157
1158 pub async fn find_invite_path(
1160 &self,
1161 proxy_inviter_address: Option<Address>,
1162 ) -> Result<PathfindingResult, SdkError> {
1163 let invitation_module = self.common.core.config.invitation_module_address;
1164 let token_to_use = match proxy_inviter_address {
1165 Some(address) => address,
1166 None => self
1167 .proxy_inviters()
1168 .await?
1169 .into_iter()
1170 .next()
1171 .map(|inviter| inviter.address)
1172 .ok_or_else(|| {
1173 SdkError::OperationFailed(format!(
1174 "no proxy inviters available for {:#x}",
1175 self.address
1176 ))
1177 })?,
1178 };
1179
1180 let path = self
1181 .common
1182 .find_path(
1183 invitation_module,
1184 invitation_fee_amount(),
1185 Some(AdvancedTransferOptions {
1186 use_wrapped_balances: Some(true),
1187 from_tokens: None,
1188 to_tokens: Some(vec![token_to_use]),
1189 exclude_from_tokens: None,
1190 exclude_to_tokens: None,
1191 simulated_balances: None,
1192 simulated_trusts: Some(vec![SimulatedTrust {
1193 truster: invitation_module,
1194 trustee: self.address,
1195 }]),
1196 max_transfers: None,
1197 tx_data: None,
1198 }),
1199 )
1200 .await?;
1201
1202 if path.transfers.is_empty() {
1203 return Err(SdkError::OperationFailed(format!(
1204 "no invitation path found from {:#x} to {:#x}",
1205 self.address, invitation_module
1206 )));
1207 }
1208
1209 if path.max_flow < invitation_fee_amount() {
1210 return Err(SdkError::OperationFailed(format!(
1211 "insufficient balance for invitation flow from {:#x}: requested {} wei, available {} wei",
1212 self.address,
1213 invitation_fee_amount(),
1214 path.max_flow
1215 )));
1216 }
1217
1218 Ok(path)
1219 }
1220
1221 pub async fn find_farm_invite_path(&self) -> Result<PathfindingResult, SdkError> {
1223 let farm_destination = farm_destination_address();
1224 let path = self
1225 .common
1226 .find_path(
1227 farm_destination,
1228 invitation_fee_amount(),
1229 Some(AdvancedTransferOptions {
1230 use_wrapped_balances: Some(true),
1231 from_tokens: None,
1232 to_tokens: Some(vec![gnosis_group_address()]),
1233 exclude_from_tokens: None,
1234 exclude_to_tokens: None,
1235 simulated_balances: None,
1236 simulated_trusts: None,
1237 max_transfers: None,
1238 tx_data: None,
1239 }),
1240 )
1241 .await?;
1242
1243 if path.transfers.is_empty() {
1244 return Err(SdkError::OperationFailed(format!(
1245 "no invitation farm path found from {:#x} to {:#x}",
1246 self.address, farm_destination
1247 )));
1248 }
1249
1250 if path.max_flow < invitation_fee_amount() {
1251 return Err(SdkError::OperationFailed(format!(
1252 "insufficient balance for invitation farm flow from {:#x}: requested {} wei, available {} wei",
1253 self.address,
1254 invitation_fee_amount(),
1255 path.max_flow
1256 )));
1257 }
1258
1259 Ok(path)
1260 }
1261
1262 pub async fn plan_invite(
1264 &self,
1265 invitee: Address,
1266 ) -> Result<Vec<PreparedTransaction>, SdkError> {
1267 let is_human = self
1268 .core
1269 .hub_v2()
1270 .isHuman(invitee)
1271 .call()
1272 .await
1273 .map_err(|e| SdkError::Contract(e.to_string()))?;
1274 if is_human {
1275 return Err(SdkError::OperationFailed(format!(
1276 "Invitee {invitee:#x} is already registered as a human in Circles Hub. Cannot invite an already registered user."
1277 )));
1278 }
1279
1280 self.plan_invitation_delivery(encode_direct_invite_data(invitee))
1281 .await
1282 }
1283
1284 pub async fn invite(&self, invitee: Address) -> Result<Vec<SubmittedTx>, SdkError> {
1286 let txs = self.plan_invite(invitee).await?;
1287 self.common.send(txs).await
1288 }
1289
1290 pub async fn plan_referral_code(&self) -> Result<ReferralCodePlan, SdkError> {
1292 let private_key = generate_private_key();
1293 let signer = private_key_to_address(&private_key)?;
1294 let txs = self
1295 .plan_invitation_delivery(encode_referral_invite_data(
1296 signer,
1297 self.common.core.config.referrals_module_address,
1298 ))
1299 .await?;
1300
1301 Ok(ReferralCodePlan {
1302 private_key,
1303 signer,
1304 txs,
1305 })
1306 }
1307
1308 pub async fn get_referral_code(&self) -> Result<ReferralCodePlan, SdkError> {
1310 self.plan_referral_code().await
1311 }
1312
1313 pub async fn invitations_from(
1315 &self,
1316 accepted: bool,
1317 ) -> Result<InvitationsFromResponse, SdkError> {
1318 Ok(self
1319 .common
1320 .rpc
1321 .invitation()
1322 .get_invitations_from(self.address, accepted)
1323 .await?)
1324 }
1325
1326 pub async fn accepted_invitees(&self) -> Result<Vec<InvitedAccountInfo>, SdkError> {
1328 Ok(self.invitations_from(true).await?.results)
1329 }
1330
1331 pub async fn pending_invitees(&self) -> Result<Vec<InvitedAccountInfo>, SdkError> {
1333 Ok(self.invitations_from(false).await?.results)
1334 }
1335
1336 pub async fn list_referrals(
1338 &self,
1339 limit: Option<u32>,
1340 offset: Option<u32>,
1341 ) -> Result<ReferralPreviewList, SdkError> {
1342 let referrals_service_url = self
1343 .common
1344 .core
1345 .config
1346 .referrals_service_url
1347 .as_deref()
1348 .ok_or_else(|| {
1349 SdkError::OperationFailed(
1350 "Referrals service not configured. Set referrals_service_url in CirclesConfig."
1351 .to_string(),
1352 )
1353 })?;
1354 let referrals = Referrals::new(referrals_service_url, self.core.clone())?;
1355
1356 Ok(referrals
1357 .list_public(
1358 self.address,
1359 Some(ReferralPublicListOptions {
1360 limit,
1361 offset,
1362 in_session: None,
1363 }),
1364 )
1365 .await?)
1366 }
1367
1368 async fn submit_generated_referrals(
1369 &self,
1370 mut generated: GeneratedInvites,
1371 ) -> Result<GeneratedInvites, SdkError> {
1372 generated.submitted = Some(self.common.send(generated.txs.clone()).await?);
1373 Ok(generated)
1374 }
1375
1376 pub async fn invitations(
1378 &self,
1379 ) -> Result<Vec<circles_rpc::methods::invitation::InvitationRow>, SdkError> {
1380 Ok(self
1381 .common
1382 .rpc
1383 .invitation()
1384 .get_invitations(self.address)
1385 .await?)
1386 }
1387
1388 pub async fn redeem_invitation(&self, inviter: Address) -> Result<Vec<SubmittedTx>, SdkError> {
1390 let call = circles_abis::InvitationEscrow::redeemInvitationCall { inviter };
1391 let tx = call_to_tx(
1392 self.common.core.config.invitation_escrow_address,
1393 call,
1394 None,
1395 );
1396 self.common.send(vec![tx]).await
1397 }
1398
1399 pub async fn revoke_invitation(&self, invitee: Address) -> Result<Vec<SubmittedTx>, SdkError> {
1401 let call = circles_abis::InvitationEscrow::revokeInvitationCall { invitee };
1402 let tx = call_to_tx(
1403 self.common.core.config.invitation_escrow_address,
1404 call,
1405 None,
1406 );
1407 self.common.send(vec![tx]).await
1408 }
1409
1410 pub async fn revoke_all_invitations(&self) -> Result<Vec<SubmittedTx>, SdkError> {
1412 let call = circles_abis::InvitationEscrow::revokeAllInvitationsCall {};
1413 let tx = call_to_tx(
1414 self.common.core.config.invitation_escrow_address,
1415 call,
1416 None,
1417 );
1418 self.common.send(vec![tx]).await
1419 }
1420
1421 pub fn new(
1422 address: Address,
1423 info: AvatarInfo,
1424 core: Arc<Core>,
1425 profiles: Profiles,
1426 rpc: Arc<CirclesRpc>,
1427 runner: Option<Arc<dyn ContractRunner>>,
1428 ) -> Self {
1429 let common = CommonAvatar::new(address, core.clone(), profiles, rpc, runner.clone());
1430 Self {
1431 address,
1432 info,
1433 core,
1434 runner,
1435 common,
1436 }
1437 }
1438}
1439
1440#[cfg(test)]
1441mod tests {
1442 use super::*;
1443 use alloy_primitives::{Bytes, TxHash, address};
1444 use async_trait::async_trait;
1445 use circles_profiles::Profiles;
1446 use circles_types::{AvatarType, CirclesConfig};
1447 use std::sync::Mutex;
1448
1449 const TEST_CID: &str = "QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG";
1450
1451 #[derive(Default)]
1452 struct RecordingRunner {
1453 sender: Address,
1454 sent: Mutex<Vec<Vec<PreparedTransaction>>>,
1455 }
1456
1457 #[async_trait]
1458 impl ContractRunner for RecordingRunner {
1459 fn sender_address(&self) -> Address {
1460 self.sender
1461 }
1462
1463 async fn send_transactions(
1464 &self,
1465 txs: Vec<PreparedTransaction>,
1466 ) -> Result<Vec<crate::SubmittedTx>, crate::RunnerError> {
1467 self.sent.lock().expect("lock").push(txs.clone());
1468 Ok(txs
1469 .into_iter()
1470 .map(|_| crate::SubmittedTx {
1471 tx_hash: Bytes::from(TxHash::ZERO.as_slice().to_vec()),
1472 success: true,
1473 index: None,
1474 })
1475 .collect())
1476 }
1477 }
1478
1479 fn dummy_config() -> CirclesConfig {
1480 CirclesConfig {
1481 circles_rpc_url: "https://rpc.example.com".into(),
1482 pathfinder_url: "https://pathfinder.example.com".into(),
1483 profile_service_url: "https://profiles.example.com".into(),
1484 referrals_service_url: None,
1485 v1_hub_address: Address::repeat_byte(0x01),
1486 v2_hub_address: Address::repeat_byte(0x02),
1487 name_registry_address: Address::repeat_byte(0x03),
1488 base_group_mint_policy: Address::repeat_byte(0x04),
1489 standard_treasury: Address::repeat_byte(0x05),
1490 core_members_group_deployer: Address::repeat_byte(0x06),
1491 base_group_factory_address: Address::repeat_byte(0x07),
1492 lift_erc20_address: Address::repeat_byte(0x08),
1493 invitation_escrow_address: Address::repeat_byte(0x09),
1494 invitation_farm_address: Address::repeat_byte(0x0a),
1495 referrals_module_address: Address::repeat_byte(0x0b),
1496 invitation_module_address: Address::repeat_byte(0x0c),
1497 }
1498 }
1499
1500 fn dummy_avatar(address: Address) -> AvatarInfo {
1501 AvatarInfo {
1502 block_number: 0,
1503 timestamp: None,
1504 transaction_index: 0,
1505 log_index: 0,
1506 transaction_hash: TxHash::ZERO,
1507 version: 2,
1508 avatar_type: AvatarType::CrcV2RegisterHuman,
1509 avatar: address,
1510 token_id: None,
1511 has_v1: false,
1512 v1_token: None,
1513 cid_v0_digest: None,
1514 cid_v0: None,
1515 v1_stopped: None,
1516 is_human: true,
1517 name: None,
1518 symbol: None,
1519 }
1520 }
1521
1522 fn test_avatar() -> (HumanAvatar, Arc<RecordingRunner>, CirclesConfig) {
1523 let config = dummy_config();
1524 let runner = Arc::new(RecordingRunner {
1525 sender: Address::repeat_byte(0xaa),
1526 sent: Mutex::new(Vec::new()),
1527 });
1528 let avatar = HumanAvatar::new(
1529 Address::repeat_byte(0xaa),
1530 dummy_avatar(Address::repeat_byte(0xaa)),
1531 Arc::new(Core::new(config.clone())),
1532 Profiles::new(config.profile_service_url.clone()).expect("profiles"),
1533 Arc::new(CirclesRpc::try_from_http(&config.circles_rpc_url).expect("rpc")),
1534 Some(runner.clone()),
1535 );
1536 (avatar, runner, config)
1537 }
1538
1539 #[test]
1540 fn referral_payload_encodes() {
1541 let signers = vec![address!("1000000000000000000000000000000000000001")];
1542 let create_accounts = ReferralsModule::createAccountsCall {
1543 signers: signers.clone(),
1544 };
1545 let payload = ReferralPayload {
1546 referralsModule: address!("2000000000000000000000000000000000000002"),
1547 callData: create_accounts.abi_encode().into(),
1548 };
1549 let bytes = payload.abi_encode();
1550 assert!(!bytes.is_empty());
1551 }
1552
1553 #[test]
1554 fn batch_tx_targets_hub() {
1555 let ids = vec![U256::from(1), U256::from(2)];
1556 let amount = U256::from(96u128) * U256::from(10).pow(U256::from(18));
1557 let values = vec![amount; ids.len()];
1558 let batch_call = HubV2::safeBatchTransferFromCall {
1559 _from: address!("aaaa000000000000000000000000000000000000"),
1560 _to: address!("bbbb000000000000000000000000000000000000"),
1561 _ids: ids,
1562 _values: values,
1563 _data: Bytes::default(),
1564 };
1565 let batch_tx = call_to_tx(
1566 address!("cccc000000000000000000000000000000000000"),
1567 batch_call,
1568 None,
1569 );
1570 assert_eq!(
1571 batch_tx.to,
1572 address!("cccc000000000000000000000000000000000000")
1573 );
1574 }
1575
1576 #[test]
1577 fn invitation_fee_matches_ts_constant() {
1578 assert_eq!(
1579 invitation_fee_amount(),
1580 U256::from(96u128) * U256::from(10).pow(U256::from(18))
1581 );
1582 }
1583
1584 #[test]
1585 fn order_proxy_inviters_prioritizes_inviter() {
1586 let inviter = Address::repeat_byte(0xaa);
1587 let ordered = order_proxy_inviters(
1588 vec![
1589 ProxyInviter {
1590 address: Address::repeat_byte(0xbb),
1591 possible_invites: 1,
1592 },
1593 ProxyInviter {
1594 address: inviter,
1595 possible_invites: 2,
1596 },
1597 ],
1598 inviter,
1599 );
1600
1601 assert_eq!(ordered[0].address, inviter);
1602 }
1603
1604 #[test]
1605 fn summarize_proxy_inviters_rewrites_wrapped_owners() {
1606 let inviter = Address::repeat_byte(0xaa);
1607 let wrapper = Address::repeat_byte(0xbb);
1608 let other = Address::repeat_byte(0xcc);
1609 let terminal = vec![
1610 PathfindingTransferStep {
1611 from: Address::repeat_byte(0x01),
1612 to: Address::repeat_byte(0x02),
1613 token_owner: format!("{wrapper:#x}"),
1614 value: invitation_fee_amount() * U256::from(2u64),
1615 },
1616 PathfindingTransferStep {
1617 from: Address::repeat_byte(0x03),
1618 to: Address::repeat_byte(0x02),
1619 token_owner: format!("{other:#x}"),
1620 value: invitation_fee_amount(),
1621 },
1622 ];
1623 let owner_remap = HashMap::from([(wrapper, inviter)]);
1624
1625 let inviters = summarize_proxy_inviters(&terminal, &owner_remap, inviter);
1626
1627 assert_eq!(inviters.len(), 2);
1628 assert_eq!(inviters[0].address, inviter);
1629 assert_eq!(inviters[0].possible_invites, 2);
1630 assert_eq!(inviters[1].address, other);
1631 assert_eq!(inviters[1].possible_invites, 1);
1632 }
1633
1634 #[test]
1635 fn direct_invite_data_encodes_single_address() {
1636 let invitee = address!("1234000000000000000000000000000000000000");
1637
1638 assert_eq!(
1639 encode_direct_invite_data(invitee),
1640 Bytes::from(invitee.abi_encode())
1641 );
1642 }
1643
1644 #[test]
1645 fn farm_constants_match_ts_values() {
1646 assert_eq!(
1647 farm_destination_address(),
1648 address!("9Eb51E6A39B3F17bB1883B80748b56170039ff1d")
1649 );
1650 assert_eq!(
1651 farm_quota_holder(),
1652 address!("20EcD8bDeb2F48d8a7c94E542aA4feC5790D9676")
1653 );
1654 }
1655
1656 #[test]
1657 fn build_inviter_setup_txs_enable_then_trust_when_module_missing() {
1658 let inviter = address!("1000000000000000000000000000000000000001");
1659 let invitation_module = address!("2000000000000000000000000000000000000002");
1660 let txs = build_inviter_setup_txs(inviter, invitation_module, false, false);
1661
1662 assert_eq!(txs.len(), 2);
1663 assert_eq!(txs[0].to, inviter);
1664 assert_eq!(
1665 &txs[0].data[..4],
1666 &SafeMinimal::enableModuleCall {
1667 module: invitation_module,
1668 }
1669 .abi_encode()[..4]
1670 );
1671 assert_eq!(txs[1].to, invitation_module);
1672 assert_eq!(
1673 &txs[1].data[..4],
1674 &InvitationModuleMinimal::trustInviterCall { inviter }.abi_encode()[..4]
1675 );
1676 }
1677
1678 #[test]
1679 fn build_inviter_setup_txs_only_trust_when_module_enabled() {
1680 let inviter = address!("3000000000000000000000000000000000000003");
1681 let invitation_module = address!("4000000000000000000000000000000000000004");
1682 let txs = build_inviter_setup_txs(inviter, invitation_module, true, false);
1683
1684 assert_eq!(txs.len(), 1);
1685 assert_eq!(txs[0].to, invitation_module);
1686 assert_eq!(
1687 &txs[0].data[..4],
1688 &InvitationModuleMinimal::trustInviterCall { inviter }.abi_encode()[..4]
1689 );
1690 }
1691
1692 #[test]
1693 fn build_inviter_setup_txs_skip_when_already_ready() {
1694 let txs = build_inviter_setup_txs(
1695 address!("5000000000000000000000000000000000000005"),
1696 address!("6000000000000000000000000000000000000006"),
1697 true,
1698 true,
1699 );
1700
1701 assert!(txs.is_empty());
1702 }
1703
1704 #[test]
1705 fn claim_invite_tx_targets_farm() {
1706 let farm = address!("7000000000000000000000000000000000000007");
1707 let tx = build_claim_invite_tx(farm);
1708
1709 assert_eq!(tx.to, farm);
1710 assert_eq!(
1711 &tx.data[..4],
1712 &InvitationFarm::claimInviteCall {}.abi_encode()[..4]
1713 );
1714 }
1715
1716 #[test]
1717 fn direct_invite_transfer_tx_matches_hub_call() {
1718 let tx = build_invitation_transfer_tx(
1719 address!("8000000000000000000000000000000000000008"),
1720 address!("9000000000000000000000000000000000000009"),
1721 address!("a00000000000000000000000000000000000000a"),
1722 U256::from(123u64),
1723 encode_direct_invite_data(address!("b00000000000000000000000000000000000000b")),
1724 );
1725
1726 let expected = HubV2::safeTransferFromCall {
1727 _from: address!("9000000000000000000000000000000000000009"),
1728 _to: address!("a00000000000000000000000000000000000000a"),
1729 _id: U256::from(123u64),
1730 _value: invitation_fee_amount(),
1731 _data: encode_direct_invite_data(address!("b00000000000000000000000000000000000000b")),
1732 };
1733
1734 assert_eq!(tx.to, address!("8000000000000000000000000000000000000008"));
1735 assert_eq!(tx.data, Bytes::from(expected.abi_encode()));
1736 assert_eq!(tx.value, None);
1737 }
1738
1739 #[test]
1740 fn referral_invite_data_encodes_create_account_payload() {
1741 let signer = address!("c00000000000000000000000000000000000000c");
1742 let referrals_module = address!("d00000000000000000000000000000000000000d");
1743 let data = encode_referral_invite_data(signer, referrals_module);
1744
1745 let expected_create_account = ReferralsModule::createAccountCall { signer };
1746 let expected_payload = ReferralPayload {
1747 referralsModule: referrals_module,
1748 callData: expected_create_account.abi_encode().into(),
1749 };
1750
1751 assert_eq!(data, Bytes::from(expected_payload.abi_encode()));
1752 }
1753
1754 #[tokio::test]
1755 async fn write_helpers_encode_expected_calls() {
1756 let (avatar, runner, config) = test_avatar();
1757
1758 avatar
1759 .update_profile_metadata(TEST_CID)
1760 .await
1761 .expect("update metadata");
1762 avatar
1763 .register_short_name(7)
1764 .await
1765 .expect("register short name");
1766 avatar.personal_mint().await.expect("personal mint");
1767 avatar.stop_mint().await.expect("stop mint");
1768
1769 let sent = runner.sent.lock().expect("lock");
1770 assert_eq!(sent.len(), 4);
1771
1772 assert_eq!(sent[0][0].to, config.name_registry_address);
1773 assert_eq!(
1774 &sent[0][0].data[..4],
1775 &circles_abis::NameRegistry::updateMetadataDigestCall {
1776 _metadataDigest: cid_v0_to_digest(TEST_CID).expect("cid"),
1777 }
1778 .abi_encode()[..4]
1779 );
1780
1781 assert_eq!(sent[1][0].to, config.name_registry_address);
1782 assert_eq!(
1783 &sent[1][0].data[..4],
1784 &circles_abis::NameRegistry::registerShortNameWithNonceCall {
1785 _nonce: U256::from(7u64),
1786 }
1787 .abi_encode()[..4]
1788 );
1789
1790 assert_eq!(sent[2][0].to, config.v2_hub_address);
1791 assert_eq!(
1792 &sent[2][0].data[..4],
1793 &HubV2::personalMintCall {}.abi_encode()[..4]
1794 );
1795
1796 assert_eq!(sent[3][0].to, config.v2_hub_address);
1797 assert_eq!(&sent[3][0].data[..4], &HubV2::stopCall {}.abi_encode()[..4]);
1798 }
1799
1800 #[tokio::test]
1801 async fn submit_generated_referrals_uses_runner_for_prepared_batch() {
1802 let (avatar, runner, _config) = test_avatar();
1803 let prepared = GeneratedInvites {
1804 secrets: vec!["0x1234".into()],
1805 signers: vec![Address::repeat_byte(0x55)],
1806 txs: vec![
1807 RunnerTx {
1808 to: Address::repeat_byte(0x11),
1809 data: Bytes::from(vec![0xaa]),
1810 value: None,
1811 },
1812 RunnerTx {
1813 to: Address::repeat_byte(0x22),
1814 data: Bytes::from(vec![0xbb]),
1815 value: None,
1816 },
1817 ],
1818 submitted: None,
1819 };
1820
1821 let result = avatar
1822 .submit_generated_referrals(prepared)
1823 .await
1824 .expect("submit generated referrals");
1825
1826 assert_eq!(result.txs.len(), 2);
1827 assert!(result.submitted.is_some());
1828
1829 let sent = runner.sent.lock().expect("lock");
1830 assert_eq!(sent.len(), 1);
1831 assert_eq!(sent[0].len(), 2);
1832 }
1833}