blokli_client/lib.rs
1//! Rust client for the Blokli GraphQL API and transaction endpoints.
2//!
3//! `blokli-client` provides a typed, async interface to a Blokli service. It wraps Blokli's GraphQL API with
4//! ergonomic Rust traits for:
5//!
6//! - querying accounts, balances, channels, safes, ticket statistics, chain metadata, and transaction state;
7//! - subscribing to server-sent event streams for accounts, channels, health, graph changes, safe deployments, ticket
8//! redemptions, and tracked transactions;
9//! - submitting signed transactions and optionally waiting for tracking or confirmation.
10//!
11//! The main entry point is [`BlokliClient`]. Most operations are trait methods, so bring the trait for the operation
12//! family into scope:
13//!
14//! - [`BlokliQueryClient`] for one-shot GraphQL queries;
15//! - [`BlokliSubscriptionClient`] for streaming subscriptions;
16//! - [`BlokliTransactionClient`] for signed transaction submission and tracking.
17//!
18//! # API concepts
19//!
20//! A [`BlokliClient`] is configured with the Blokli service base URL, not the GraphQL endpoint itself. For example,
21//! `https://blokli.example.org` becomes `https://blokli.example.org/graphql` for GraphQL requests and SSE
22//! subscriptions.
23//!
24//! Queries and subscriptions use typed selectors instead of generic filter maps. Address-like values such as
25//! [`ChainAddress`], [`ChannelId`], [`PacketKey`], and [`TxReceipt`] are byte arrays at the public boundary and are
26//! encoded as hex strings for GraphQL. Blokli-specific identifiers such as [`KeyId`] and [`TxId`] are kept distinct:
27//! a [`TxId`] is a Blokli tracking id, while a [`TxReceipt`] is the on-chain transaction hash returned by submission
28//! endpoints.
29//!
30//! Subscriptions are GraphQL operations delivered over server-sent events. They yield streams of `Result<T, E>` so
31//! callers can decide how to handle item-level errors. Transaction helpers operate on already-signed raw transaction
32//! bytes; this crate does not sign transactions.
33//!
34//! # Quick start
35//!
36//! Create a client with a Blokli base URL. The client derives the GraphQL endpoint by appending `/graphql` to that
37//! base URL.
38//!
39//! ```no_run
40//! use blokli_client::{BlokliClient, BlokliClientConfig, BlokliQueryClient};
41//!
42//! async fn example() -> Result<(), Box<dyn std::error::Error>> {
43//! let client = BlokliClient::new("https://blokli.example.org".parse()?, BlokliClientConfig::default());
44//!
45//! let version = client.query_version().await?;
46//! let chain = client.query_chain_info().await?;
47//!
48//! println!("Blokli {version} indexes chain {}", chain.chain_id);
49//! Ok(())
50//! }
51//! ```
52//!
53//! # Querying
54//!
55//! Selectors are strongly typed. For example, channel queries use [`ChannelSelector`] plus an optional
56//! [`ChannelFilter`] and status.
57//!
58//! ```no_run
59//! use blokli_client::{BlokliClient, BlokliClientConfig, BlokliQueryClient, ChannelFilter, ChannelSelector};
60//!
61//! async fn example(source: u32) -> Result<(), Box<dyn std::error::Error>> {
62//! let client = BlokliClient::new("https://blokli.example.org".parse()?, BlokliClientConfig::default());
63//! let selector = ChannelSelector {
64//! filter: Some(ChannelFilter::SourceKeyId(source)),
65//! ..Default::default()
66//! };
67//!
68//! let channels = client.query_channels(selector).await?;
69//! println!("{} channels found", channels.channels.len());
70//! Ok(())
71//! }
72//! ```
73//!
74//! # Subscriptions
75//!
76//! Subscription methods return [`futures::Stream`] values. Streams use Blokli's SSE endpoint and yield `Result` items
77//! so callers can decide how to handle transient transport, parsing, or GraphQL errors.
78//!
79//! ```no_run
80//! use blokli_client::{AccountSelector, BlokliClient, BlokliClientConfig, BlokliSubscriptionClient};
81//! use futures::TryStreamExt;
82//!
83//! async fn example() -> Result<(), Box<dyn std::error::Error>> {
84//! let client = BlokliClient::new("https://blokli.example.org".parse()?, BlokliClientConfig::default());
85//! let mut accounts = Box::pin(client.subscribe_accounts(AccountSelector::Any)?);
86//!
87//! while let Some(account) = accounts.try_next().await? {
88//! println!("account key id: {}", account.keyid);
89//! }
90//!
91//! Ok(())
92//! }
93//! ```
94//!
95//! # Transactions
96//!
97//! Transaction methods expect already-signed raw transactions. `submit_transaction` returns immediately after
98//! submission, `submit_and_track_transaction` returns a Blokli tracking id, and `submit_and_confirm_transaction`
99//! waits for the requested number of confirmations.
100//!
101//! ```no_run
102//! use std::time::Duration;
103//!
104//! use blokli_client::{BlokliClient, BlokliClientConfig, BlokliTransactionClient};
105//!
106//! async fn example(signed_transaction: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
107//! let client = BlokliClient::new("https://blokli.example.org".parse()?, BlokliClientConfig::default());
108//!
109//! let tx_id = client.submit_and_track_transaction(signed_transaction).await?;
110//! let transaction = client.track_transaction(tx_id, Duration::from_secs(120)).await?;
111//!
112//! println!("transaction status: {:?}", transaction.status);
113//! Ok(())
114//! }
115//! ```
116//!
117//! # Public API layout
118//!
119//! Common selectors, traits, and address aliases are re-exported at the crate root. The versioned API remains available
120//! under [`api::v1`], and GraphQL response models are grouped under [`types`].
121//!
122//! # Errors
123//!
124//! Fallible operations return [`BlokliClientError`]. Use [`BlokliClientError::kind`] when matching on stable client
125//! error categories such as invalid input, GraphQL errors, timeouts, or transaction tracking failures.
126//!
127//! # DNS override
128//!
129//! By default, [`BlokliClient`] uses the system DNS resolver through `reqwest`. Callers that need to keep Blokli
130//! communication working while DNS is unreliable can configure [`BlokliClientConfig::dns_override`] to pin the Blokli
131//! URL hostname to a fixed IP address.
132//!
133//! The request hostname is not rewritten. For example, a client configured with `https://blokli.example.org` and a DNS
134//! override still sends requests for `blokli.example.org`, preserving TLS SNI and certificate validation while
135//! bypassing system DNS for that hostname. When [`BlokliDnsOverride::port`] is set, it becomes the request port;
136//! otherwise the original URL port or scheme default is used.
137//!
138//! ```no_run
139//! use std::net::IpAddr;
140//!
141//! use blokli_client::{BlokliClient, BlokliClientConfig, BlokliDnsOverride};
142//!
143//! let client = BlokliClient::new(
144//! "https://blokli.example.org".parse()?,
145//! BlokliClientConfig {
146//! dns_override: Some(BlokliDnsOverride {
147//! ip: IpAddr::from([203, 0, 113, 10]),
148//! port: None,
149//! }),
150//! ..Default::default()
151//! },
152//! );
153//! # let _ = client;
154//! # Ok::<(), Box<dyn std::error::Error>>(())
155//! ```
156//!
157//! Leave `dns_override` as `None` to use normal system DNS resolution.
158/// Current Blokli client API.
159pub mod api;
160mod client;
161/// Errors returned by the Blokli client.
162pub mod errors;
163
164/// Version of the `blokli-client` crate.
165pub const CLIENT_VERSION: &str = env!("CARGO_PKG_VERSION");
166
167pub use api::{
168 AccountSelector, BlokliQueryClient, BlokliSubscriptionClient, BlokliTransactionClient, ChainAddress, ChannelFilter,
169 ChannelId, ChannelSelector, KeyId, ModulePredictionInput, PacketKey, RedeemedStatsSelector, SafeSelector,
170 ServiceSelector, ServiceTypeId, TicketSelector, TxId, TxReceipt, types,
171};
172pub use client::{BlokliClient, BlokliClientConfig, BlokliDnsOverride, ReqwestTransport};
173#[cfg(feature = "testing")]
174pub use client::{
175 BlokliTestClient, BlokliTestState, BlokliTestStateMutator, BlokliTestStateSnapshot, GraphQlQueries, NopStateMutator,
176};
177pub use errors::{BlokliClientError, ErrorKind, TrackingErrorKind};
178
179#[cfg(feature = "testing")]
180pub mod internal {
181 pub use super::api::internal::*;
182}
183
184#[doc(hidden)]
185pub mod exports {
186 pub use url::Url;
187 #[cfg(feature = "testing")]
188 pub use {
189 cynic::{Operation, StreamingOperation},
190 indexmap::{IndexMap, map::Entry},
191 };
192}