Skip to main content

blokli_client/api/v1/
mod.rs

1//! Blokli API v1 client contract.
2//!
3//! This module contains the public trait surface implemented by [`BlokliClient`](crate::BlokliClient), plus the
4//! selectors and response models used by those traits.
5//!
6//! # Trait families
7//!
8//! Use these traits to make the corresponding methods available on [`BlokliClient`](crate::BlokliClient):
9//!
10//! - [`BlokliQueryClient`] for request/response GraphQL queries.
11//! - [`BlokliSubscriptionClient`] for SSE-backed GraphQL subscriptions.
12//! - [`BlokliTransactionClient`] for signed transaction submission and tracking.
13//!
14//! # Selectors
15//!
16//! Query and subscription methods avoid unstructured filter maps. Instead, pass one of the selector types:
17//!
18//! - [`AccountSelector`] selects accounts by key id, chain address, packet key, or all accounts.
19//! - [`ChannelSelector`] combines an optional [`ChannelFilter`], channel status, and safe address.
20//! - [`SafeSelector`] selects safes by safe address, owner, chain key alias, or registered node.
21//! - [`RedeemedStatsSelector`] selects ticket redemption aggregates.
22//! - [`ServiceSelector`] selects service registry entries by service type, node, or both.
23//! - [`TicketSelector`] filters ticket redemption subscription events.
24//!
25//! # Response models
26//!
27//! The [`types`] module re-exports the schema-facing GraphQL response structs and enums that are returned by public
28//! methods. These models intentionally mirror the Blokli API shape while conversions at the client boundary normalize
29//! common success/error unions into `Result` values.
30//!
31//! # Example
32//!
33//! ```no_run
34//! use blokli_client::{BlokliClient, BlokliClientConfig, BlokliQueryClient, ChannelFilter, ChannelSelector};
35//!
36//! async fn example(destination: u32) -> Result<(), Box<dyn std::error::Error>> {
37//!     let client = BlokliClient::new("https://blokli.example.org".parse()?, BlokliClientConfig::default());
38//!     let selector = ChannelSelector {
39//!         filter: Some(ChannelFilter::DestinationKeyId(destination)),
40//!         ..Default::default()
41//!     };
42//!     let stats = client.query_channel_stats(selector).await?;
43//!
44//!     println!("{} channels matched", stats.count);
45//!     Ok(())
46//! }
47//! ```
48
49use std::{fmt::Formatter, time::Duration};
50
51pub(crate) mod graphql;
52pub mod types {
53    #[cfg(feature = "curvy")]
54    pub use super::graphql::curvy::{
55        CurvyAddress, CurvyAggregatorFees, CurvyAggregatorState, CurvyBooleanValue, CurvyCommittedNote,
56        CurvyCommittedNotes, CurvyCommittedNullifier, CurvyCommittedNullifiers, CurvyEventCursor, CurvyEventPosition,
57        CurvyGasFees, CurvyNoteStatus, CurvyPendingNote, CurvyPendingNotes, CurvyShardRoot, CurvyShardRootPage,
58        CurvySyncCheckpoint, CurvySyncNote, CurvySyncNotePage, CurvySyncNullifierPage, CurvyVaultFees, CurvyVaultToken,
59        CurvyVaultTokenCount,
60    };
61    pub use super::graphql::{
62        ChannelStatus, DateTime, Hex32, ReadinessState, Token, TokenValueString, Uint64, Uint256,
63        accounts::Account,
64        balances::{HoprBalance, NativeBalance, RedeemedStats, SafeHoprAllowance},
65        channels::{Channel, ChannelStats, ChannelsList, SafesBalance},
66        graph::OpenedChannelsGraphEntry,
67        info::{ChainInfo, Compatibility, ContractAddressMap, TicketParameters},
68        safe::{ModuleAddress, Safe},
69        services::{
70            ServiceEntry, ServiceRegistryConfig, ServiceTypeInfo, ServiceTypeUpdate, ServiceTypeUpdateKind,
71            ServiceUpdate, ServiceUpdateKind,
72        },
73        tickets::{RedeemTicketDetails, RedemptionResult},
74        txs::{SafeExecution, Transaction, TransactionStatus},
75    };
76}
77
78pub(crate) mod internal {
79    #[cfg(feature = "curvy")]
80    pub use super::graphql::curvy::{
81        CurvyCheckpointVariables, CurvyEntryPortalVariables, CurvyEventPageVariables, CurvyEventSubscriptionVariables,
82        CurvyExitPortalVariables, CurvyNoteIdVariables, CurvyNullifierVariables, CurvyPortalVariables,
83        CurvyRootVariables, CurvySyncPageVariables, CurvyVaultTokenVariables, QueryCurvyAggregatorFees,
84        QueryCurvyAggregatorState, QueryCurvyCommittedNotes, QueryCurvyCommittedNullifiers,
85        QueryCurvyEntryPortalAddress, QueryCurvyExitPortalAddress, QueryCurvyNoteStatus, QueryCurvyNullifierSpent,
86        QueryCurvyPendingNotes, QueryCurvyPortalRegistered, QueryCurvyShardRoots, QueryCurvySyncCheckpoint,
87        QueryCurvySyncNotes, QueryCurvySyncNullifiers, QueryCurvyValidNotesRoot, QueryCurvyVaultFees,
88        QueryCurvyVaultToken, QueryCurvyVaultTokenCount, SubscribeCurvyCommittedNote, SubscribeCurvyCommittedNullifier,
89        SubscribeCurvyPendingNote,
90    };
91    pub use super::graphql::{
92        accounts::{
93            AccountVariables, QueryAccountCount, QueryAccounts, QueryTxCount, SubscribeAccounts, TxCountVariables,
94        },
95        balances::{
96            BalanceVariables, QueryHoprBalance, QueryNativeBalance, QueryRedeemedStats, QuerySafeAllowance,
97            RedeemedStatsFilter, RedeemedStatsVariables,
98        },
99        channels::{
100            ChannelStatsVariables, ChannelsVariables, QueryChannelCount, QueryChannelStats, QueryChannels,
101            QuerySafesBalance, SafesBalanceVariables, SubscribeChannels,
102        },
103        graph::SubscribeGraph,
104        info::{QueryChainInfo, QueryCompatibility, QueryHealth, QueryVersion, SubscribeHealth, SubscribeTicketParams},
105        safe::{
106            ModuleAddressVariables, QueryModuleAddress, QuerySafeBy, SafeByVariables, SafeSelectorInput,
107            SubscribeSafeDeployment,
108        },
109        services::{
110            QueryServiceCount, QueryServiceRegistryConfig, QueryServiceTypes, QueryServices, ServicePageVariables,
111            ServiceTypeVariables, ServiceVariables, SubscribeServiceRegistryConfig, SubscribeServiceTypes,
112            SubscribeServices,
113        },
114        tickets::{SubscribeTicketRedeemed, TicketRedeemedVariables},
115        txs::{
116            ConfirmTransactionVariables, MutateConfirmTransaction, MutateSendTransaction, MutateTrackTransaction,
117            QueryTransaction, SendTransactionVariables, SubscribeTransaction, TransactionsVariables,
118        },
119    };
120}
121
122/// EVM-style 20-byte chain address used by Blokli account, safe, and node filters.
123pub type ChainAddress = [u8; 20];
124/// HOPR packet key used to identify accounts.
125pub type PacketKey = [u8; 32];
126/// Concrete 32-byte payment channel identifier.
127pub type ChannelId = [u8; 32];
128/// Service type identifier used by the on-chain service registry.
129///
130/// The registry stores the identifier as a raw `bytes32`. By convention it holds right-padded
131/// printable ASCII, so `gvpn:exit` is
132/// `0x6776706e3a657869740000000000000000000000000000000000000000000000`, but the contract does not
133/// enforce that, and any non-zero 32-byte value can appear on chain. Blokli renders the identifier
134/// as its ASCII name when it follows the convention and as `0x`-prefixed hex otherwise, so the
135/// string fields of [`ServiceEntry`](types::ServiceEntry) and
136/// [`ServiceTypeInfo`](types::ServiceTypeInfo) may hold either form.
137pub type ServiceTypeId = [u8; 32];
138/// Transaction receipt or hash returned by transaction submission endpoints.
139pub type TxReceipt = [u8; 32];
140/// Numeric Blokli key id.
141pub type KeyId = u32;
142/// Blokli transaction tracking identifier.
143///
144/// This id is returned by [`BlokliTransactionClient::submit_and_track_transaction`] and can be passed to
145/// [`BlokliQueryClient::query_transaction_status`], [`BlokliSubscriptionClient::subscribe_track_transaction`], or
146/// [`BlokliTransactionClient::track_transaction`].
147pub type TxId = String;
148
149/// Selects [`Account`](types::Account) records by key id, chain address, packet key, or all accounts.
150///
151/// `AccountSelector::Any` is accepted by [`BlokliQueryClient::count_accounts`] and
152/// [`BlokliSubscriptionClient::subscribe_accounts`]. [`BlokliQueryClient::query_accounts`] requires a narrower
153/// selector to avoid accidentally fetching an unbounded account list.
154#[derive(Clone)]
155pub enum AccountSelector {
156    /// Select an account by its key id.
157    KeyId(KeyId),
158    /// Select an account by its on-chain address.
159    Address(ChainAddress),
160    /// Select an account by its packet key.
161    PacketKey(PacketKey),
162    /// Matches any account.
163    Any,
164}
165
166impl std::fmt::Debug for AccountSelector {
167    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
168        match self {
169            Self::KeyId(key_id) => write!(f, "KeyId({})", key_id),
170            Self::Address(address) => write!(f, "Address({})", hex::encode(address)),
171            Self::PacketKey(packet_key) => write!(f, "PacketKey({})", hex::encode(packet_key)),
172            AccountSelector::Any => write!(f, "Any"),
173        }
174    }
175}
176
177/// Selects [`Channel`](types::Channel) records by optional channel filter, status, and safe contract address.
178///
179/// Use [`ChannelSelector::default`] to address all channels when a method supports unfiltered access. Query methods
180/// that could otherwise return large result sets may require at least one filter.
181#[derive(Debug, Clone, Default)]
182pub struct ChannelSelector {
183    /// Filter for the selected channels.
184    pub filter: Option<ChannelFilter>,
185    /// Optional status filter for the selected channels.
186    pub status: Option<types::ChannelStatus>,
187    /// Optional safe contract address; restricts to channels where the source belongs to this safe.
188    pub safe_address: Option<ChainAddress>,
189}
190
191impl ChannelSelector {
192    /// Returns `true` if the selector matches any channel.
193    pub fn matches_all(&self) -> bool {
194        self.filter.is_none() && self.status.is_none() && self.safe_address.is_none()
195    }
196}
197
198/// Filters [`Channel`](types::Channel) records by channel id, source key id, destination key id, or both endpoint key
199/// ids.
200#[derive(Clone)]
201pub enum ChannelFilter {
202    /// Select a channel by its channel id.
203    ChannelId(ChannelId),
204    /// Select channels by its destination key id.
205    DestinationKeyId(KeyId),
206    /// Select channels by its source key id.
207    SourceKeyId(KeyId),
208    /// Select channels by both source and destination key id.
209    SourceAndDestinationKeyIds(KeyId, KeyId),
210}
211
212impl std::fmt::Debug for ChannelFilter {
213    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
214        match self {
215            Self::ChannelId(channel_id) => write!(f, "ChannelId({})", hex::encode(channel_id)),
216            Self::DestinationKeyId(key_id) => write!(f, "DestinationKeyId({})", key_id),
217            Self::SourceKeyId(key_id) => write!(f, "SourceKeyId({})", key_id),
218            Self::SourceAndDestinationKeyIds(source_key_id, destination_key_id) => write!(
219                f,
220                "SourceAndDestinationKeyIds({}, {})",
221                source_key_id, destination_key_id
222            ),
223        }
224    }
225}
226
227/// Selects deployed [`Safe`](types::Safe) records by safe address, owner, chain key alias, or registered node.
228#[derive(Clone)]
229pub enum SafeSelector {
230    /// Select a safe by its address.
231    SafeAddress(ChainAddress),
232    /// Select a safe by a current owner address.
233    Owner(ChainAddress),
234    /// Select a safe by the owner's chain key legacy alias.
235    ChainKey(ChainAddress),
236    /// Select a safe by any of the registered nodes.
237    RegisteredNode(ChainAddress),
238}
239
240impl std::fmt::Debug for SafeSelector {
241    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
242        match self {
243            Self::SafeAddress(address) => write!(f, "SafeAddress({})", hex::encode(address)),
244            Self::Owner(address) => write!(f, "Owner({})", hex::encode(address)),
245            Self::ChainKey(address) => write!(f, "ChainKey({})", hex::encode(address)),
246            Self::RegisteredNode(address) => write!(f, "RegisteredNode({})", hex::encode(address)),
247        }
248    }
249}
250
251/// Selects [`ServiceEntry`](types::ServiceEntry) records by service type, node address, or both.
252///
253/// `ServiceSelector::Any` is accepted by [`BlokliQueryClient::count_services`] and
254/// [`BlokliSubscriptionClient::subscribe_services`]. [`BlokliQueryClient::query_services`] requires
255/// a narrower selector: the registry is permissionless and anyone can grow it, so a bare
256/// enumeration is not offered.
257#[derive(Clone, Copy)]
258pub enum ServiceSelector {
259    /// Select every entry of one service type.
260    ServiceType(ServiceTypeId),
261    /// Select every entry offered by one node.
262    Node(ChainAddress),
263    /// Select the single entry for one service type and one node.
264    ServiceTypeAndNode {
265        /// Service type identifier.
266        service_type: ServiceTypeId,
267        /// Node chain address.
268        node: ChainAddress,
269    },
270    /// Matches any registry entry.
271    Any,
272}
273
274impl std::fmt::Debug for ServiceSelector {
275    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
276        match self {
277            Self::ServiceType(service_type) => write!(f, "ServiceType({})", hex::encode(service_type)),
278            Self::Node(node) => write!(f, "Node({})", hex::encode(node)),
279            Self::ServiceTypeAndNode { service_type, node } => write!(
280                f,
281                "ServiceTypeAndNode(service_type={}, node={})",
282                hex::encode(service_type),
283                hex::encode(node)
284            ),
285            Self::Any => write!(f, "Any"),
286        }
287    }
288}
289
290/// Allows querying redeemed ticket aggregates by safe address, node address, or both.
291#[derive(Clone, Copy)]
292pub enum RedeemedStatsSelector {
293    /// Aggregate all rows for the given safe address.
294    SafeAddress(ChainAddress),
295    /// Aggregate all rows for the given node address.
296    NodeAddress(ChainAddress),
297    /// Return the single row matching the given safe/node pair.
298    SafeAndNodeAddress {
299        /// Safe contract address.
300        safe_address: ChainAddress,
301        /// Node address.
302        node_address: ChainAddress,
303    },
304}
305
306impl std::fmt::Debug for RedeemedStatsSelector {
307    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
308        match self {
309            Self::SafeAddress(safe) => write!(f, "SafeAddress({})", hex::encode(safe)),
310            Self::NodeAddress(node) => write!(f, "NodeAddress({})", hex::encode(node)),
311            Self::SafeAndNodeAddress {
312                safe_address,
313                node_address,
314            } => write!(
315                f,
316                "SafeAndNodeAddress(safe={}, node={})",
317                hex::encode(safe_address),
318                hex::encode(node_address)
319            ),
320        }
321    }
322}
323
324/// Filters which ticket redemption events are delivered by a [`BlokliSubscriptionClient::subscribe_ticket_redeemed`]
325/// subscription.
326///
327/// Pass one of the variants to receive only events matching that criterion, or [`TicketSelector::Any`] to receive all
328/// events.
329///
330/// # Examples
331///
332/// ```ignore
333/// use blokli_client::api::v1::{TicketSelector, ChannelId, ChainAddress};
334///
335/// // Subscribe to all redemptions in a specific channel
336/// let by_channel = TicketSelector::ChannelId(channel_id);
337///
338/// // Subscribe to all redemptions where a specific node is the issuer
339/// let by_issuer = TicketSelector::IssuerAddress(issuer_address);
340///
341/// // Subscribe to every redemption event regardless of channel or party
342/// let any = TicketSelector::Any;
343/// ```
344#[derive(Clone)]
345pub enum TicketSelector {
346    /// Filter by channel id.
347    ChannelId(ChannelId),
348    /// Filter by issuer (source node) address.
349    IssuerAddress(ChainAddress),
350    /// Filter by recipient (destination node) address.
351    RecipientAddress(ChainAddress),
352    /// Matches any ticket redemption event.
353    Any,
354}
355
356impl std::fmt::Debug for TicketSelector {
357    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
358        match self {
359            Self::ChannelId(channel_id) => write!(f, "ChannelId({})", hex::encode(channel_id)),
360            Self::IssuerAddress(address) => write!(f, "IssuerAddress({})", hex::encode(address)),
361            Self::RecipientAddress(address) => write!(f, "RecipientAddress({})", hex::encode(address)),
362            Self::Any => write!(f, "Any"),
363        }
364    }
365}
366
367/// Input for [`BlokliQueryClient::query_module_address_prediction`].
368#[derive(Clone, PartialEq, Eq)]
369pub struct ModulePredictionInput {
370    /// Safe deployment nonce.
371    pub nonce: u64,
372    /// Owner of the deployed Safe.
373    pub owner: ChainAddress,
374    /// Predicted Safe address.
375    pub safe_address: ChainAddress,
376}
377
378impl std::fmt::Debug for ModulePredictionInput {
379    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
380        f.debug_struct("ModulePredictionInput")
381            .field("nonce", &self.nonce)
382            .field("owner", &hex::encode(self.owner))
383            .field("safe_address", &hex::encode(self.safe_address))
384            .finish()
385    }
386}
387
388pub(crate) type Result<T> = std::result::Result<T, crate::errors::BlokliClientError>;
389
390/// One-shot GraphQL queries against a Blokli instance.
391///
392/// These methods return the current indexed state known to Blokli at request time. They do not subscribe for later
393/// changes and they do not retry application-level GraphQL errors. Transport, decoding, invalid input, and Blokli
394/// GraphQL union errors are returned as [`BlokliClientError`](crate::errors::BlokliClientError).
395#[async_trait::async_trait]
396pub trait BlokliQueryClient {
397    #[cfg(feature = "curvy")]
398    /// Returns a chain-ordered page of Curvy pending notes.
399    ///
400    /// `after` is exclusive and `first` must be between 1 and 1000. Pending
401    /// notes are ownership candidates; callers must filter them with Curvy's SDK.
402    async fn query_curvy_pending_notes(
403        &self,
404        from_block: Option<u64>,
405        after: Option<types::CurvyEventCursor>,
406        first: u32,
407    ) -> Result<types::CurvyPendingNotes>;
408    #[cfg(feature = "curvy")]
409    /// Returns a chain-ordered page of committed Curvy notes.
410    async fn query_curvy_committed_notes(
411        &self,
412        from_block: Option<u64>,
413        after: Option<types::CurvyEventCursor>,
414        first: u32,
415    ) -> Result<types::CurvyCommittedNotes>;
416    #[cfg(feature = "curvy")]
417    /// Returns a chain-ordered page of committed Curvy nullifiers.
418    async fn query_curvy_committed_nullifiers(
419        &self,
420        from_block: Option<u64>,
421        after: Option<types::CurvyEventCursor>,
422        first: u32,
423    ) -> Result<types::CurvyCommittedNullifiers>;
424    #[cfg(feature = "curvy")]
425    /// Returns the latest finalized Curvy sync checkpoint, or the checkpoint pinned by block hash.
426    async fn query_curvy_sync_checkpoint(&self, block_hash: Option<String>) -> Result<types::CurvySyncCheckpoint>;
427    #[cfg(feature = "curvy")]
428    /// Returns a checkpoint-pinned page of dense committed notes.
429    async fn query_curvy_sync_notes(
430        &self,
431        checkpoint: String,
432        from_index: Option<u64>,
433        first: u32,
434    ) -> Result<types::CurvySyncNotePage>;
435    #[cfg(feature = "curvy")]
436    /// Returns a checkpoint-pinned page of dense committed nullifiers.
437    async fn query_curvy_sync_nullifiers(
438        &self,
439        checkpoint: String,
440        from_index: Option<u64>,
441        first: u32,
442    ) -> Result<types::CurvySyncNullifierPage>;
443    #[cfg(feature = "curvy")]
444    /// Returns a checkpoint-pinned page of completed notes-tree shard roots.
445    async fn query_curvy_shard_roots(
446        &self,
447        checkpoint: String,
448        from_index: Option<u64>,
449        first: u32,
450    ) -> Result<types::CurvyShardRootPage>;
451    #[cfg(feature = "curvy")]
452    /// Reads current Curvy Aggregator indices and notes-tree root from chain.
453    async fn query_curvy_aggregator_state(&self) -> Result<types::CurvyAggregatorState>;
454    #[cfg(feature = "curvy")]
455    /// Reads a Curvy note's raw status from chain.
456    async fn query_curvy_note_status(&self, note_id: String) -> Result<types::CurvyNoteStatus>;
457    #[cfg(feature = "curvy")]
458    /// Checks whether a Curvy notes-tree root is valid.
459    async fn query_curvy_valid_notes_root(&self, root: String) -> Result<bool>;
460    #[cfg(feature = "curvy")]
461    /// Checks whether a Curvy nullifier has already been spent.
462    async fn query_curvy_nullifier_spent(&self, nullifier: String) -> Result<bool>;
463    #[cfg(feature = "curvy")]
464    /// Reads Curvy Vault protocol-level fees.
465    async fn query_curvy_vault_fees(&self) -> Result<types::CurvyVaultFees>;
466    #[cfg(feature = "curvy")]
467    /// Reads Curvy Aggregator proof fee configuration.
468    async fn query_curvy_aggregator_fees(&self) -> Result<types::CurvyAggregatorFees>;
469    #[cfg(feature = "curvy")]
470    /// Reads the number of registered Curvy Vault tokens.
471    async fn query_curvy_vault_token_count(&self) -> Result<types::CurvyVaultTokenCount>;
472    #[cfg(feature = "curvy")]
473    /// Reads one Curvy Vault token and its gas fee configuration.
474    async fn query_curvy_vault_token(&self, token_id: String) -> Result<types::CurvyVaultToken>;
475    #[cfg(feature = "curvy")]
476    /// Derives a Curvy entry portal address.
477    async fn query_curvy_entry_portal_address(&self, owner_hash: String, recovery: String) -> Result<String>;
478    #[cfg(feature = "curvy")]
479    /// Derives a Curvy exit portal address.
480    async fn query_curvy_exit_portal_address(
481        &self,
482        exit_address: String,
483        exit_chain_id: String,
484        recovery: String,
485    ) -> Result<String>;
486    #[cfg(feature = "curvy")]
487    /// Checks whether a portal is registered with Curvy PortalFactory.
488    async fn query_curvy_portal_registered(&self, portal_address: String) -> Result<bool>;
489    /// Counts accounts matching the given [`AccountSelector`].
490    ///
491    /// [`AccountSelector::Any`] is accepted here and counts every indexed account.
492    async fn count_accounts(&self, selector: AccountSelector) -> Result<u32>;
493    /// Returns accounts matching the given [`AccountSelector`].
494    ///
495    /// Unlike [`count_accounts`](BlokliQueryClient::count_accounts), this method rejects [`AccountSelector::Any`] to
496    /// avoid accidentally fetching an unbounded account list.
497    async fn query_accounts(&self, selector: AccountSelector) -> Result<Vec<types::Account>>;
498    /// Returns the native-chain balance for an account or safe address.
499    ///
500    /// The address is encoded as hexadecimal for the GraphQL request. Invalid addresses or upstream query failures are
501    /// surfaced as client errors.
502    async fn query_native_balance(&self, address: &ChainAddress) -> Result<types::NativeBalance>;
503    /// Returns the HOPR token balance for an account or safe address.
504    async fn query_token_balance(&self, address: &ChainAddress, token: types::Token) -> Result<types::HoprBalance>;
505    /// Returns the number of indexed transactions sent from the given address.
506    async fn query_transaction_count(&self, address: &ChainAddress) -> Result<u64>;
507    /// Returns the HOPR token allowance configured for a safe address.
508    async fn query_safe_allowance(&self, address: &ChainAddress) -> Result<types::SafeHoprAllowance>;
509    /// Returns redeemed and rejected ticket aggregates filtered by safe, node, or both.
510    ///
511    /// Use [`RedeemedStatsSelector::SafeAndNodeAddress`] when a single safe/node pair is required.
512    async fn query_redeemed_stats(&self, selector: RedeemedStatsSelector) -> Result<types::RedeemedStats>;
513    /// Returns deployed safes matching the given [`SafeSelector`].
514    async fn query_safe(&self, selector: SafeSelector) -> Result<Vec<types::Safe>>;
515    /// Returns the predicted module address for the given safe deployment data.
516    async fn query_module_address_prediction(&self, input: ModulePredictionInput) -> Result<ChainAddress>;
517    /// Counts channels matching the given [`ChannelSelector`].
518    ///
519    /// Prefer [`query_channel_stats`](BlokliQueryClient::query_channel_stats), which also returns the aggregate
520    /// channel balance.
521    #[deprecated(
522        since = "0.22.0",
523        note = "Use query_channel_stats instead, which returns both count and total wxHOPR balance."
524    )]
525    async fn count_channels(&self, selector: ChannelSelector) -> Result<u32>;
526    /// Returns channel count and total wxHOPR balance matching the given [`ChannelSelector`].
527    ///
528    /// An unfiltered selector returns stats across all indexed channels.
529    async fn query_channel_stats(&self, selector: ChannelSelector) -> Result<types::ChannelStats>;
530    /// Returns channels matching the given [`ChannelSelector`].
531    ///
532    /// At least one filter or safe address must be set. For unfiltered aggregate data, use
533    /// [`query_channel_stats`](BlokliQueryClient::query_channel_stats).
534    async fn query_channels(&self, selector: ChannelSelector) -> Result<types::ChannelsList>;
535    /// Returns the total wxHOPR balance held across indexed safe contracts.
536    ///
537    /// When `owner_address` is provided, restricts to safes whose registered accounts have that chain key.
538    async fn query_safes_balance(&self, owner_address: Option<ChainAddress>) -> Result<types::SafesBalance>;
539    /// Counts service registry entries matching the given [`ServiceSelector`].
540    ///
541    /// [`ServiceSelector::Any`] is accepted here and counts every indexed entry.
542    async fn count_services(&self, selector: ServiceSelector) -> Result<u32>;
543    /// Returns service registry entries matching the given [`ServiceSelector`].
544    ///
545    /// Unlike [`count_services`](BlokliQueryClient::count_services), this method rejects
546    /// [`ServiceSelector::Any`]: the registry is permissionless and anyone can grow it, so a bare
547    /// enumeration is not offered.
548    async fn query_services(&self, selector: ServiceSelector) -> Result<Vec<types::ServiceEntry>>;
549    /// Returns only entries whose node is currently bound in the NodeSafeRegistry selected by the
550    /// service registry itself.
551    async fn query_live_services(&self, selector: ServiceSelector) -> Result<Vec<types::ServiceEntry>>;
552    /// Returns service type configuration, optionally restricted to a single type.
553    ///
554    /// Passing `None` returns every registered type. Unlike the entry set, the set of types is
555    /// gated by the registry-wide type registration fee, so enumerating it is bounded.
556    async fn query_service_types(&self, service_type: Option<ServiceTypeId>) -> Result<Vec<types::ServiceTypeInfo>>;
557    /// Returns the current registry-wide type registration fee and node-safe registry pointer.
558    ///
559    /// This is the one-shot alternative to
560    /// [`BlokliSubscriptionClient::subscribe_service_registry_config`].
561    async fn query_service_registry_config(&self) -> Result<types::ServiceRegistryConfig>;
562    /// Returns the latest known status for a tracked transaction id.
563    ///
564    /// The `tx_id` is the Blokli tracking id returned by
565    /// [`BlokliTransactionClient::submit_and_track_transaction`], not necessarily the on-chain transaction hash.
566    async fn query_transaction_status(&self, tx_id: TxId) -> Result<types::Transaction>;
567    /// Returns chain, contract, fee, ticket, and timing parameters reported by Blokli.
568    async fn query_chain_info(&self) -> Result<types::ChainInfo>;
569    /// Returns the Blokli server version string.
570    async fn query_version(&self) -> Result<String>;
571    /// Returns the current health state as reported by the legacy health query.
572    async fn query_health(&self) -> Result<String>;
573    /// Queries server compatibility information.
574    ///
575    /// Legacy endpoint. `supported_client_versions` is always `"*"` on current servers,
576    /// meaning any client version is accepted.
577    async fn query_compatibility(&self) -> Result<types::Compatibility>;
578}
579
580/// SSE-backed GraphQL subscriptions to Blokli updates.
581///
582/// Subscription methods return streams of `Result<T, BlokliClientError>`. The client uses the configured reconnect,
583/// read-timeout, TCP keepalive, and restart-delay options from [`BlokliClientConfig`](crate::BlokliClientConfig).
584/// Transport issues may be retried internally; malformed GraphQL payloads and terminal stream errors are yielded as
585/// stream items so callers can decide whether to continue, log, or abort.
586pub trait BlokliSubscriptionClient {
587    /// Streams channel updates matching the given [`ChannelSelector`].
588    ///
589    /// An unfiltered selector subscribes to all channel updates. Each yielded item is a single updated channel.
590    fn subscribe_channels(
591        &self,
592        selector: ChannelSelector,
593    ) -> Result<impl futures::Stream<Item = Result<types::Channel>> + Send>;
594    /// Streams account updates matching the given [`AccountSelector`].
595    ///
596    /// [`AccountSelector::Any`] subscribes to all account updates.
597    fn subscribe_accounts(
598        &self,
599        selector: AccountSelector,
600    ) -> Result<impl futures::Stream<Item = Result<types::Account>> + Send>;
601    /// Streams updates for the open-channel graph.
602    ///
603    /// The initial stream emits one entry per currently open channel. Later updates
604    /// include all channel state transitions, including `CLOSED` entries. Consumers
605    /// should merge entries by `channel.concrete_channel_id` and use closed entries
606    /// as removal signals for an open-channel graph.
607    fn subscribe_graph(&self) -> Result<impl futures::Stream<Item = Result<types::OpenedChannelsGraphEntry>> + Send>;
608    /// Streams updates of ticket price and winning-probability parameters.
609    fn subscribe_ticket_params(&self) -> Result<impl futures::Stream<Item = Result<types::TicketParameters>> + Send>;
610    /// Streams readiness updates for the Blokli instance.
611    fn subscribe_health(&self) -> Result<impl futures::Stream<Item = Result<types::ReadinessState>> + Send>;
612    /// Streams on-chain safe deployments indexed by Blokli.
613    fn subscribe_safe_deployments(&self) -> Result<impl futures::Stream<Item = Result<types::Safe>> + Send>;
614    /// Streams changes to service registry entries matching the given [`ServiceSelector`].
615    ///
616    /// Each item reports one registration, update, or deregistration. Deregistration carries no
617    /// entry, because the entry no longer exists; the service type and node on the
618    /// [`ServiceUpdate`](types::ServiceUpdate) identify what was removed.
619    ///
620    /// [`ServiceSelector::Any`] subscribes to every registry change.
621    fn subscribe_services(
622        &self,
623        selector: ServiceSelector,
624    ) -> Result<impl futures::Stream<Item = Result<types::ServiceUpdate>> + Send>;
625    /// Streams changes to service type and registry-wide configuration.
626    ///
627    /// Passing `None` subscribes to every type. The two registry-wide kinds,
628    /// [`RegistrationFeeChanged`](types::ServiceTypeUpdateKind::RegistrationFeeChanged) and
629    /// [`RegistryPointerChanged`](types::ServiceTypeUpdateKind::RegistryPointerChanged), carry no
630    /// service type and report their new state on
631    /// [`registry_config`](types::ServiceTypeUpdate::registry_config).
632    fn subscribe_service_types(
633        &self,
634        service_type: Option<ServiceTypeId>,
635    ) -> Result<impl futures::Stream<Item = Result<types::ServiceTypeUpdate>> + Send>;
636    /// Streams the complete registry-wide configuration.
637    ///
638    /// The first item is the current type registration fee and node-safe registry pointer. Later
639    /// items contain the complete configuration after either value changes, so callers do not need
640    /// a separate query before subscribing.
641    fn subscribe_service_registry_config(
642        &self,
643    ) -> Result<impl futures::Stream<Item = Result<types::ServiceRegistryConfig>> + Send + 'static>;
644    /// Streams status updates for a tracked transaction id.
645    ///
646    /// The `tx_id` is the Blokli tracking id returned by
647    /// [`BlokliTransactionClient::submit_and_track_transaction`].
648    fn subscribe_track_transaction(
649        &self,
650        tx_id: TxId,
651    ) -> Result<impl futures::Stream<Item = Result<types::Transaction>> + Send>;
652    /// Subscribes to on-chain ticket redemption events matching the given [`TicketSelector`].
653    ///
654    /// Returns an infinite stream of `Result<`[`types::RedeemTicketDetails`]`>`. Each item represents
655    /// one redemption event that passed the selector filter. The stream terminates when the
656    /// underlying SSE connection closes; errors (network, parse) are yielded as `Err` items.
657    ///
658    /// Use [`TicketSelector::Any`] to receive every redemption, or narrow by channel, issuer, or
659    /// recipient address.
660    ///
661    /// # Examples
662    ///
663    /// ```ignore
664    /// use futures::StreamExt;
665    /// use blokli_client::api::v1::{BlokliSubscriptionClient, TicketSelector};
666    ///
667    /// let mut stream = client
668    ///     .subscribe_ticket_redeemed(TicketSelector::Any)
669    ///     .expect("failed to subscribe");
670    ///
671    /// while let Some(result) = stream.next().await {
672    ///     match result {
673    ///         Ok(event) => println!("redeemed ticket {} in epoch {}", event.index, event.epoch),
674    ///         Err(e) => eprintln!("stream error: {e}"),
675    ///     }
676    /// }
677    /// ```
678    fn subscribe_ticket_redeemed(
679        &self,
680        selector: TicketSelector,
681    ) -> Result<impl futures::Stream<Item = Result<types::RedeemTicketDetails>> + Send>;
682    #[cfg(feature = "curvy")]
683    /// Streams all pending Curvy notes from an optional inclusive block.
684    ///
685    /// Blokli does not know which node owns a note. The connector must pass each
686    /// note to the Curvy SDK ownership scanner and retain only matching note IDs.
687    /// Because `from_block` is inclusive, reconnecting consumers must deduplicate by
688    /// [`types::CurvyEventPosition`] or catch up through the exclusive paginated cursor.
689    fn subscribe_curvy_pending_notes(
690        &self,
691        from_block: Option<u64>,
692    ) -> Result<impl futures::Stream<Item = Result<types::CurvyPendingNote>> + Send>;
693    #[cfg(feature = "curvy")]
694    /// Streams all committed Curvy notes from an optional inclusive block.
695    ///
696    /// The connector must discard committed notes whose note IDs were not retained
697    /// after successful local ownership detection.
698    /// Because `from_block` is inclusive, reconnecting consumers must deduplicate
699    /// previously processed positions.
700    fn subscribe_curvy_committed_notes(
701        &self,
702        from_block: Option<u64>,
703    ) -> Result<impl futures::Stream<Item = Result<types::CurvyCommittedNote>> + Send>;
704    #[cfg(feature = "curvy")]
705    /// Streams committed Curvy nullifiers from an optional inclusive block.
706    fn subscribe_curvy_committed_nullifiers(
707        &self,
708        from_block: Option<u64>,
709    ) -> Result<impl futures::Stream<Item = Result<types::CurvyCommittedNullifier>> + Send>;
710}
711
712/// Signed transaction submission and tracking through Blokli.
713///
714/// These methods do not sign transactions. Callers provide raw signed transaction bytes. Submission success means
715/// Blokli accepted or relayed the transaction according to the chosen mode; callers that need durable chain state
716/// should rely on confirmations or independent chain observation.
717#[async_trait::async_trait]
718pub trait BlokliTransactionClient {
719    /// Submits a signed transaction and returns the on-chain transaction hash reported by Blokli.
720    ///
721    /// This method does not wait for confirmation.
722    async fn submit_transaction(&self, signed_tx: &[u8]) -> Result<TxReceipt>;
723    /// Submits a signed transaction and returns a Blokli tracking id.
724    ///
725    /// Pass the returned id to [`BlokliQueryClient::query_transaction_status`],
726    /// [`BlokliSubscriptionClient::subscribe_track_transaction`], or
727    /// [`track_transaction`](BlokliTransactionClient::track_transaction).
728    async fn submit_and_track_transaction(&self, signed_tx: &[u8]) -> Result<TxId>;
729    /// Submits a signed transaction and waits for the requested number of confirmations.
730    ///
731    /// Blokli caps very large confirmation counts internally. A timeout or RPC error is returned as
732    /// [`BlokliClientError`](crate::errors::BlokliClientError).
733    async fn submit_and_confirm_transaction(&self, signed_tx: &[u8], num_confirmations: usize) -> Result<TxReceipt>;
734    /// Tracks the transaction given the `tx_id` previously returned
735    /// by [`submit_and_track_transaction`](BlokliTransactionClient::submit_and_track_transaction) until it is confirmed
736    /// or [fails](crate::errors::TrackingErrorKind).
737    async fn track_transaction(&self, tx_id: TxId, client_timeout: Duration) -> Result<types::Transaction>;
738}