circles-sdk 0.1.1

Circles SDK orchestrating RPC, profiles, pathfinding, transfers, and contract runners.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
//! Circles SDK orchestrating RPC, profile service access, pathfinding, transfers,
//! and optional contract execution.
//!
//! This crate mirrors the high-level TypeScript SDK shape while keeping the Rust
//! implementation read-first: most reads work with `Sdk::new(config, None)`, and
//! write paths are gated behind a [`ContractRunner`].
//!
//! ## Quick Start
//!
//! ```rust,no_run
//! use alloy_primitives::address;
//! use circles_sdk::{Sdk, config};
//!
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//! let sdk = Sdk::new(config::gnosis_mainnet(), None)?;
//! let avatar = address!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
//! let info = sdk.avatar_info(avatar).await?;
//! println!("avatar type: {:?}", info.avatar_type);
//!
//! let typed = sdk.get_avatar(avatar).await?;
//! match typed {
//!     circles_sdk::Avatar::Human(human) => {
//!         let balances = human.balances(false, true).await?;
//!         println!("balances: {}", balances.len());
//!     }
//!     circles_sdk::Avatar::Organisation(_) | circles_sdk::Avatar::Group(_) => {}
//! }
//! # Ok(())
//! # }
//! ```
//!
//! ## Usage Model
//!
//! - [`Sdk`] wires together RPC, profile lookups, pathfinding, transfers, and contract bindings.
//! - [`Avatar`] gives you a typed wrapper after runtime avatar detection.
//! - [`ContractRunner`] is only required for write paths such as registrations, trust changes,
//!   and transfer submission.
//! - [`SafeContractRunner`] and [`EoaContractRunner`] are the built-in execution backends for
//!   existing single-owner Safe wallets and direct EOA execution, and now expose buffered batch,
//!   gas-estimation, and read-call helper surface on the runner itself.
//! - [`SafeExecutionBuilder`] is the browser/external-signature foundation for Safe-backed
//!   flows: it prepares the canonical Safe payload/hash without requiring a local private key.
//! - The optional `ws` feature enables WebSocket subscriptions with retry/backoff and HTTP catch-up helpers.
//!
//! ## Recommended Entry Points
//!
//! - [`config::gnosis_mainnet`] for the shared mainnet configuration.
//! - [`Sdk::avatar_info`] for a fast read-only probe.
//! - [`Sdk::get_avatar`] when you want a typed avatar wrapper.
//! - [`EoaContractRunner::connect`] and [`SafeContractRunner::connect`] when you want built-in
//!   execution backends for existing wallets.
//! - [`SafeExecutionBuilder::connect`] when you need the TS-style Safe transaction-preparation
//!   seam before an external/browser signer submits the transaction.
//! - [`HumanAvatar::plan_transfer`], [`OrganisationAvatar::plan_transfer`], and
//!   [`BaseGroupAvatar::plan_transfer`] for pathfinding-based transaction planning.
//! - [`HumanAvatar::plan_direct_transfer`], [`OrganisationAvatar::plan_direct_transfer`], and
//!   [`BaseGroupAvatar::plan_direct_transfer`] for TS-style direct-send planning.
//! - [`HumanAvatar::plan_group_token_redeem`] and
//!   [`OrganisationAvatar::plan_group_token_redeem`] for automatic group-token redeem planning.
//! - [`HumanAvatar::available_invitations`], [`HumanAvatar::invitation_origin`],
//!   [`HumanAvatar::proxy_inviters`], and [`HumanAvatar::find_farm_invite_path`] for the
//!   current invitation/referral query surface.
//! - [`HumanAvatar::plan_invite`] and [`HumanAvatar::invite`] for TS-style direct invite
//!   planning/execution against existing Safe wallets.
//! - [`Sdk::referrals`] and [`HumanAvatar::list_referrals`] for the optional referrals backend.
//! - [`HumanAvatar::plan_referral_code`] and [`HumanAvatar::get_referral_code`] for the
//!   TS-style single-referral planner used by `getReferralCode()`.
//! - [`HumanAvatar::plan_generate_referrals`] and [`HumanAvatar::generate_referrals`] for
//!   invitation-farm batch referral planning/execution.
//!
//! ## Validation
//!
//! - Unit tests: `cargo test -p circles-sdk`
//! - WS helpers: `cargo test -p circles-sdk --features ws`
//! - Live checks (ignored by default): `RUN_LIVE=1 LIVE_AVATAR=0x... cargo test -p circles-sdk -- --ignored`

mod avatar;
mod cid_v0_to_digest;
pub mod config;
mod core;
mod runner;
mod services;
#[cfg(feature = "ws")]
pub mod ws;
pub use services::referrals::{
    Referral, ReferralInfo, ReferralList, ReferralListMineOptions, ReferralPreview,
    ReferralPreviewList, ReferralPublicListOptions, ReferralSession, ReferralStatus,
    ReferralStoreInput, ReferralSyncStatus, Referrals, ReferralsError, StoreBatchError,
    StoreBatchResult,
};
pub use services::registration;

#[cfg(feature = "ws")]
use alloy_json_rpc::RpcSend;
use alloy_primitives::Address;
pub use avatar::{BaseGroupAvatar, HumanAvatar, OrganisationAvatar};
use circles_profiles::{Profile, Profiles};
#[cfg(feature = "ws")]
use circles_rpc::events::subscription::CirclesSubscription;
use circles_rpc::{CirclesRpc, PagedQuery};
#[cfg(feature = "ws")]
use circles_types::CirclesEvent;
use circles_types::{
    AggregatedTrustRelation, AvatarInfo, AvatarType, CirclesConfig, GroupMembershipRow,
    GroupTokenHolderRow, SortOrder, TokenBalanceResponse, TrustRelation,
};
use core::Core;
pub use runner::{
    BatchRun, ContractRunner, EoaContractRunner, PreparedSafeExecution, PreparedTransaction,
    RunnerError, SafeContractRunner, SafeExecutionBuilder, SubmittedTx, call_to_tx,
};
#[cfg(feature = "ws")]
use serde_json::to_value;
use std::sync::Arc;
use thiserror::Error;

/// Generic registration outcome carrying submitted transactions and an optional avatar.
///
/// Registration helpers may return prepared txs without sending if no runner is provided.
pub struct RegistrationResult<T> {
    /// Best-effort typed avatar returned after registration succeeds.
    pub avatar: Option<T>,
    /// Submitted transactions returned by the runner.
    pub txs: Vec<SubmittedTx>,
}

/// High-level SDK errors.
#[derive(Debug, Error)]
pub enum SdkError {
    #[error("circles rpc error: {0}")]
    Rpc(#[from] circles_rpc::CirclesRpcError),
    #[error("profiles error: {0}")]
    Profiles(#[from] circles_profiles::ProfilesError),
    #[error("referrals error: {0}")]
    Referrals(#[from] services::referrals::ReferralsError),
    #[error("transfers error: {0}")]
    Transfers(#[from] circles_transfers::TransferError),
    #[error("runner error: {0}")]
    Runner(#[from] RunnerError),
    #[error("cid error: {0}")]
    Cid(#[from] cid_v0_to_digest::CidError),
    #[error("contract call error: {0}")]
    Contract(String),
    #[error("operation failed: {0}")]
    OperationFailed(String),
    #[error("contract runner is required for this operation")]
    MissingRunner,
    #[error("sender address is required for this operation")]
    MissingSender,
    #[error("avatar not found for address {0:?}")]
    AvatarNotFound(Address),
    #[error("invalid registration input: {0}")]
    InvalidRegistration(String),
    #[error("websocket subscription failed after {attempts} attempts: {reason}")]
    WsSubscribeFailed { attempts: usize, reason: String },
}

/// Top-level SDK orchestrator.
///
/// Construct this once per config/runner pair and reuse it across read and write flows.
pub struct Sdk {
    pub(crate) config: CirclesConfig,
    pub(crate) rpc: Arc<CirclesRpc>,
    pub(crate) profiles: Profiles,
    pub(crate) referrals: Option<Referrals>,
    pub(crate) core: Arc<Core>,
    pub(crate) runner: Option<Arc<dyn ContractRunner>>,
    pub(crate) sender_address: Option<Address>,
}

impl Sdk {
    /// Create a new SDK instance. Provide a runner for write operations; omit for read-only.
    pub fn new(
        config: CirclesConfig,
        runner: Option<Arc<dyn ContractRunner>>,
    ) -> Result<Self, SdkError> {
        let sender_address = runner.as_ref().map(|r| r.sender_address());
        let core = Arc::new(Core::new(config.clone()));
        let rpc = Arc::new(CirclesRpc::try_from_http(&config.circles_rpc_url)?);
        let profiles = Profiles::new(config.profile_service_url.clone())?;
        let referrals = config
            .referrals_service_url
            .as_deref()
            .map(|url| Referrals::new(url, core.clone()))
            .transpose()?;
        Ok(Self {
            rpc,
            profiles,
            referrals,
            config,
            core,
            runner,
            sender_address,
        })
    }

    /// Access the underlying RPC client.
    pub fn rpc(&self) -> &CirclesRpc {
        self.rpc.as_ref()
    }

    /// Access the loaded configuration.
    pub fn config(&self) -> &CirclesConfig {
        &self.config
    }

    /// Access core contract bundle.
    pub fn core(&self) -> &Arc<Core> {
        &self.core
    }

    /// Access the profiles client.
    pub fn profiles(&self) -> &Profiles {
        &self.profiles
    }

    /// Optional referrals client when `referrals_service_url` is configured.
    pub fn referrals(&self) -> Option<&Referrals> {
        self.referrals.as_ref()
    }

    /// Optional runner.
    pub fn runner(&self) -> Option<&Arc<dyn ContractRunner>> {
        self.runner.as_ref()
    }

    /// Sender address derived from the runner.
    pub fn sender_address(&self) -> Option<Address> {
        self.sender_address
    }

    /// Create and pin a profile via the profile service.
    ///
    /// This only talks to the profile service and does not submit any on-chain transaction.
    pub async fn create_profile(&self, profile: &Profile) -> Result<String, SdkError> {
        Ok(self.profiles.create(profile).await?)
    }

    /// Fetch a profile by CID (returns `Ok(None)` if missing or unparsable).
    pub async fn get_profile(&self, cid: &str) -> Result<Option<Profile>, SdkError> {
        Ok(self.profiles.get(cid).await?)
    }

    /// Read avatar metadata directly from the RPC service.
    pub async fn data_avatar(&self, avatar: Address) -> Result<AvatarInfo, SdkError> {
        Ok(self.rpc.avatar().get_avatar_info(avatar).await?)
    }

    /// Read trust relations for an avatar directly from the RPC service.
    pub async fn data_trust(&self, avatar: Address) -> Result<Vec<TrustRelation>, SdkError> {
        Ok(self.rpc.trust().get_trust_relations(avatar).await?)
    }

    /// Read aggregated trust relations for an avatar directly from the RPC service.
    pub async fn data_trust_aggregated(
        &self,
        avatar: Address,
    ) -> Result<Vec<AggregatedTrustRelation>, SdkError> {
        Ok(self
            .rpc
            .trust()
            .get_aggregated_trust_relations(avatar)
            .await?)
    }

    /// Read token balances for an avatar directly from the RPC service.
    ///
    /// Set `as_time_circles` to request balances in time-Circles units and `use_v2`
    /// to scope the query to v2 balances.
    pub async fn data_balances(
        &self,
        avatar: Address,
        as_time_circles: bool,
        use_v2: bool,
    ) -> Result<Vec<TokenBalanceResponse>, SdkError> {
        Ok(self
            .rpc
            .token()
            .get_token_balances(avatar, as_time_circles, use_v2)
            .await?)
    }

    /// Get all members of a specific group via the shared paged query helper.
    pub fn group_members(
        &self,
        group: Address,
        limit: u32,
        sort_order: SortOrder,
    ) -> PagedQuery<GroupMembershipRow> {
        self.rpc.group().get_group_members(group, limit, sort_order)
    }

    /// Get collateral balances held in a group's treasury.
    pub async fn group_collateral(
        &self,
        group: Address,
    ) -> Result<Vec<TokenBalanceResponse>, SdkError> {
        let treasury = self
            .core
            .base_group(group)
            .BASE_TREASURY()
            .call()
            .await
            .map_err(|e| SdkError::Contract(e.to_string()))?;
        Ok(self
            .rpc
            .token()
            .get_token_balances(treasury, false, true)
            .await?)
    }

    /// Get holders of a group token ordered like the TypeScript helper.
    pub fn group_holders(&self, group: Address, limit: u32) -> PagedQuery<GroupTokenHolderRow> {
        self.rpc.group().get_group_holders(group, limit)
    }

    /// Convenience accessor for avatar info (read-only).
    pub async fn avatar_info(&self, avatar: Address) -> Result<AvatarInfo, SdkError> {
        Ok(self.rpc.avatar().get_avatar_info(avatar).await?)
    }

    /// Subscribe to Circles events over WebSocket with a custom JSON-RPC filter payload.
    #[cfg(feature = "ws")]
    pub async fn subscribe_events_ws<F>(
        &self,
        ws_url: &str,
        filter: F,
    ) -> Result<CirclesSubscription<CirclesEvent>, SdkError>
    where
        F: RpcSend + 'static,
    {
        let val = to_value(&filter).map_err(|e| SdkError::WsSubscribeFailed {
            attempts: 0,
            reason: e.to_string(),
        })?;
        self.subscribe_events_ws_with_retries(ws_url, val, None)
            .await
    }

    /// Subscribe with retry/backoff on WebSocket connection or subscription failure.
    #[cfg(feature = "ws")]
    pub async fn subscribe_events_ws_with_retries(
        &self,
        ws_url: &str,
        filter: serde_json::Value,
        max_attempts: Option<usize>,
    ) -> Result<CirclesSubscription<CirclesEvent>, SdkError> {
        ws::subscribe_with_retries(ws_url, filter, max_attempts).await
    }

    /// Subscribe with retry/backoff and optionally fetch historical events first over HTTP.
    #[cfg(feature = "ws")]
    pub async fn subscribe_events_ws_with_catchup(
        &self,
        ws_url: &str,
        filter: serde_json::Value,
        max_attempts: Option<usize>,
        catch_up_from_block: Option<u64>,
        catch_up_filter: Option<Vec<circles_types::Filter>>,
    ) -> Result<(Vec<CirclesEvent>, CirclesSubscription<CirclesEvent>), SdkError> {
        ws::subscribe_with_catchup(
            self.rpc.as_ref(),
            ws_url,
            filter,
            max_attempts,
            catch_up_from_block,
            catch_up_filter,
            None,
        )
        .await
    }

    /// Fetch avatar info and return the matching typed avatar wrapper.
    ///
    /// Unknown or personal avatar types are treated as [`Avatar::Human`] to match the
    /// current SDK behavior.
    pub async fn get_avatar(&self, avatar: Address) -> Result<Avatar, SdkError> {
        let info = self.rpc.avatar().get_avatar_info(avatar).await?;
        Ok(match info.avatar_type {
            AvatarType::CrcV2RegisterGroup => Avatar::Group(BaseGroupAvatar::new(
                avatar,
                info,
                self.core.clone(),
                self.profiles.clone(),
                self.rpc.clone(),
                self.runner.clone(),
            )),
            AvatarType::CrcV2RegisterOrganization => Avatar::Organisation(OrganisationAvatar::new(
                avatar,
                info,
                self.core.clone(),
                self.profiles.clone(),
                self.rpc.clone(),
                self.runner.clone(),
            )),
            _ => Avatar::Human(HumanAvatar::new(
                avatar,
                info,
                self.core.clone(),
                self.profiles.clone(),
                self.rpc.clone(),
                self.runner.clone(),
            )),
        })
    }

    /// Register a human avatar (profile is pinned before submission). Requires a runner.
    pub async fn register_human(
        &self,
        inviter: Address,
        profile: &Profile,
    ) -> Result<RegistrationResult<HumanAvatar>, SdkError> {
        registration::register_human(self, inviter, profile).await
    }

    /// Register an organisation avatar. Requires a runner.
    pub async fn register_organisation(
        &self,
        name: &str,
        profile: &Profile,
    ) -> Result<RegistrationResult<OrganisationAvatar>, SdkError> {
        registration::register_organisation(self, name, profile).await
    }

    /// Register a base group via the factory. Returns submitted txs and best-effort avatar.
    #[allow(clippy::too_many_arguments)]
    pub async fn register_group(
        &self,
        owner: Address,
        service: Address,
        fee_collection: Address,
        initial_conditions: &[Address],
        name: &str,
        symbol: &str,
        profile: &Profile,
    ) -> Result<RegistrationResult<BaseGroupAvatar>, SdkError> {
        registration::register_group(
            self,
            owner,
            service,
            fee_collection,
            initial_conditions,
            name,
            symbol,
            profile,
        )
        .await
    }
}

/// Top-level avatar enum (human, organisation, group).
pub enum Avatar {
    /// Human or personal avatar wrapper.
    Human(HumanAvatar),
    /// Organisation avatar wrapper.
    Organisation(OrganisationAvatar),
    /// Base group avatar wrapper.
    Group(BaseGroupAvatar),
}