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}