Skip to main content

circles_sdk/avatar/
human.rs

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
31/// Top-level avatar enum variant: human.
32pub struct HumanAvatar {
33    /// Avatar address on-chain.
34    pub address: Address,
35    /// RPC-derived avatar metadata.
36    pub info: AvatarInfo,
37    /// Shared contract bundle and configuration.
38    pub core: Arc<Core>,
39    /// Optional runner used for write-capable flows.
40    pub runner: Option<Arc<dyn ContractRunner>>,
41    /// Shared read/write helper implementation.
42    pub common: CommonAvatar,
43}
44
45/// Invitation generation result (secrets + signers + prepared txs).
46#[derive(Debug, Clone)]
47pub struct GeneratedInvites {
48    /// Generated invitation secrets in hex form.
49    pub secrets: Vec<String>,
50    /// Derived signer addresses associated with the generated secrets.
51    pub signers: Vec<Address>,
52    /// Prepared transactions required to mint/fund the invites.
53    pub txs: Vec<RunnerTx>,
54    /// Submitted transactions when invite generation was executed through a runner.
55    pub submitted: Option<Vec<RunnerSubmitted>>,
56}
57
58/// Single referral-code planning result for the TS `getReferralCode` flow.
59#[derive(Debug, Clone)]
60pub struct ReferralCodePlan {
61    /// Generated referral private key to share with the invitee.
62    pub private_key: String,
63    /// Signer address derived from the generated private key.
64    pub signer: Address,
65    /// Prepared transactions required to activate the referral on-chain.
66    pub txs: Vec<PreparedTransaction>,
67}
68
69/// Proxy inviter candidate used by the TS invitation planner surface.
70#[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    /// Get detailed token balances (v1/v2 selectable).
346    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    /// Get aggregate balance (v1/v2 selectable).
355    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    /// Get trust relations.
364    pub async fn trust_relations(&self) -> Result<Vec<TrustRelation>, SdkError> {
365        self.common.trust_relations().await
366    }
367
368    /// Get aggregated trust relations.
369    pub async fn aggregated_trust_relations(
370        &self,
371    ) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
372        self.common.aggregated_trust_relations().await
373    }
374
375    /// Get outgoing trust relations only.
376    pub async fn trusts(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
377        self.common.trusts().await
378    }
379
380    /// Get incoming trust relations only.
381    pub async fn trusted_by(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
382        self.common.trusted_by().await
383    }
384
385    /// Get mutual trust relations only.
386    pub async fn mutual_trusts(&self) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
387        self.common.mutual_trusts().await
388    }
389
390    /// Check whether this avatar trusts `other_avatar`.
391    pub async fn is_trusting(&self, other_avatar: Address) -> Result<bool, SdkError> {
392        self.common.is_trusting(other_avatar).await
393    }
394
395    /// Check whether `other_avatar` trusts this avatar.
396    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    /// Fetch profile (cached by CID in memory).
401    pub async fn profile(&self) -> Result<Option<Profile>, SdkError> {
402        self.common.profile(self.info.cid_v0.as_deref()).await
403    }
404
405    /// Get transaction history for this avatar using cursor-based pagination.
406    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    /// Update profile via profiles service and store CID through NameRegistry (requires runner).
415    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    /// Update the on-chain profile CID pointer through NameRegistry (requires runner).
421    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    /// Register a short name using a specific nonce (requires runner).
431    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    /// Trust one or more avatars via HubV2::trust (requires runner).
440    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    /// Remove trust (sets expiry to 0). Requires runner.
458    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    /// Plan a transfer without submitting.
504    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    /// Execute a transfer using the runner (requires runner).
514    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    /// Plan a direct transfer without pathfinding.
524    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    /// Execute a direct transfer using the runner (requires runner).
537    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    /// Plan a replenish flow without submitting.
550    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    /// Execute a replenish flow using the runner (requires runner).
560    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    /// Compute the maximum amount that can be replenished into this human's own token.
570    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    /// Plan a replenish flow for the maximum currently replenishable amount.
599    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    /// Execute a replenish flow for the maximum currently replenishable amount.
613    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    /// Plan a group-token mint by routing collateral to the group's mint handler.
622    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    /// Execute a group-token mint by routing collateral to the group's mint handler.
653    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    /// Plan a group-token redeem flow back into trusted treasury collateral.
663    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    /// Execute a group-token redeem flow back into trusted treasury collateral.
672    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    /// Compute the maximum amount mintable for a group from this avatar.
681    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    /// Find a path from this avatar to `to` for the requested target flow.
709    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    /// Compute the maximum available flow from this avatar to `to`.
719    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    /// Get the owner address for a group.
728    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    /// Get the mint handler address for a group.
738    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    /// Get the treasury address for a group.
748    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    /// Get the service address for a group.
758    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    /// Get the fee collection address for a group.
768    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    /// Get all membership conditions for a group.
778    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    /// Get group memberships for this avatar using the shared paged-query helper.
791    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    /// Fetch all group memberships, then resolve full group rows for the referenced groups.
803    ///
804    /// This mirrors the TS helper shape: `limit` is used as the page size for the
805    /// membership query, not as a hard cap on the final enriched result count.
806    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    /// Mint all currently claimable personal tokens (requires runner).
841    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    /// Permanently stop personal token minting (requires runner).
848    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        // Simulate claim to retrieve ids
865        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        // Secrets/signers
883        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        // Referral payload
893        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        // Amounts: 96 CRC each
904        let amount = invitation_fee_amount();
905        let values = vec![amount; ids.len()];
906
907        // Build txs: claimInvites + safeBatchTransferFrom to invitation module
908        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    /// Plan batch referral generation via the invitation farm without submitting.
951    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    /// Backward-compatible alias for the older plan-only helper name.
959    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    /// Execute batch referral generation using the configured runner.
967    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    /// Invitation fee in atto-circles (96 CRC), matching the TS helper constant.
976    pub fn invitation_fee(&self) -> U256 {
977        invitation_fee_amount()
978    }
979
980    /// Invitation module address currently configured on the invitation farm.
981    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    /// Remaining invitation quota available to this avatar on the invitation farm.
992    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    /// Compute the deterministic Safe address used by the referrals module for a signer.
1003    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    /// Unified invitation-origin helper for this avatar.
1014    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    /// Direct inviter for this avatar when available.
1024    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    /// Combined invitation availability for this avatar across trust, escrow, and at-scale sources.
1034    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    /// Proxy inviters that can route invitations to the invitation module.
1047    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    /// Find an invitation path to the configured invitation module, optionally forcing a proxy inviter.
1159    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    /// Find a fallback path from this avatar to the invitation farm destination.
1222    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    /// Plan a direct invite flow for an existing Safe wallet without submitting.
1263    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    /// Execute a direct invite flow using the configured runner.
1285    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    /// Plan the TS-style single-referral flow and return the generated private key.
1291    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    /// Backward-compatible TS-style name for the single-referral planner.
1309    pub async fn get_referral_code(&self) -> Result<ReferralCodePlan, SdkError> {
1310        self.plan_referral_code().await
1311    }
1312
1313    /// Accounts invited by this avatar, filtered by accepted vs pending status.
1314    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    /// Accepted invitees for this avatar.
1327    pub async fn accepted_invitees(&self) -> Result<Vec<InvitedAccountInfo>, SdkError> {
1328        Ok(self.invitations_from(true).await?.results)
1329    }
1330
1331    /// Pending invitees for this avatar.
1332    pub async fn pending_invitees(&self) -> Result<Vec<InvitedAccountInfo>, SdkError> {
1333        Ok(self.invitations_from(false).await?.results)
1334    }
1335
1336    /// Public referral previews created by this avatar via the optional referrals backend.
1337    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    /// Legacy invitation-balance rows (RPC helper).
1377    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    /// Redeem an invitation from an inviter (requires runner).
1389    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    /// Revoke a specific invitation (requires runner).
1400    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    /// Revoke all invitations sent by this avatar (requires runner).
1411    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}