Skip to content

Commit 9818572

Browse files
committed
Encapsulate LightClient with typestate
I made an attempt in the base client to use typestate for running the node [here](2140-dev/kyoto#535). While I think the pattern is ergonomic, it imposes too many restrictions on the `Client` ownership model. The pattern does seem appropriate here, however, where we are taking a more opinionated approach as to how the user interacts with the light client. This API abstracts the `Node` entirely for users that have a `tokio` environment present. I divided the stages of the `LightClient` into `Idle`, `Subscribed`, `Active`. The differentiation between `Idle` and `Subscribed` can be seen in the example. This allows the user to set up the necessary logging tasks required for their app. Then they may call `start` to execute the task, and finally make requests thereafter.
1 parent c619621 commit 9818572

File tree

4 files changed

+187
-77
lines changed

4 files changed

+187
-77
lines changed

examples/example.rs

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use bdk_kyoto::builder::{Builder, BuilderExt};
2-
use bdk_kyoto::{
3-
HeaderCheckpoint, Info, LightClient, Receiver, ScanType, UnboundedReceiver, Warning,
4-
};
2+
use bdk_kyoto::{HeaderCheckpoint, Info, Receiver, ScanType, UnboundedReceiver, Warning};
53
use bdk_wallet::bitcoin::Network;
64
use bdk_wallet::{KeychainKind, Wallet};
75
use tokio::select;
@@ -56,18 +54,15 @@ async fn main() -> anyhow::Result<()> {
5654
};
5755

5856
// The light client builder handles the logic of inserting the SPKs
59-
let LightClient {
60-
requester,
61-
info_subscriber,
62-
warning_subscriber,
63-
mut update_subscriber,
64-
node,
65-
} = Builder::new(NETWORK)
57+
let client = Builder::new(NETWORK)
6658
.build_with_wallet(&wallet, scan_type)
6759
.unwrap();
68-
69-
tokio::task::spawn(async move { node.run().await });
70-
tokio::task::spawn(async move { traces(info_subscriber, warning_subscriber).await });
60+
let (client, logging, mut update_subscriber) = client.subscribe();
61+
tokio::task::spawn(
62+
async move { traces(logging.info_subscriber, logging.warning_subscriber).await },
63+
);
64+
let client = client.start();
65+
let requester = client.requester();
7166

7267
// Sync and apply updates. We can do this in a continual loop while the "application" is running.
7368
// Often this would occur on a separate thread than the underlying application user interface.

src/builder.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,7 @@
3030
//!
3131
//! let scan_type = ScanType::Sync;
3232
//!
33-
//! let LightClient {
34-
//! requester,
35-
//! info_subscriber,
36-
//! warning_subscriber,
37-
//! update_subscriber,
38-
//! node
39-
//! } = Builder::new(Network::Signet)
33+
//! let client = Builder::new(Network::Signet)
4034
//! // A node may handle multiple connections
4135
//! .required_peers(2)
4236
//! // Choose where to store node data
@@ -59,7 +53,7 @@ use bdk_wallet::{
5953
pub use bip157::Builder;
6054
use bip157::{chain::ChainState, HeaderCheckpoint};
6155

62-
use crate::{LightClient, ScanType, UpdateSubscriber};
56+
use crate::{Idle, LightClient, LoggingSubscribers, ScanType, UpdateSubscriber};
6357

6458
const IMPOSSIBLE_REORG_DEPTH: usize = 7;
6559

@@ -70,15 +64,15 @@ pub trait BuilderExt {
7064
self,
7165
wallet: &Wallet,
7266
scan_type: ScanType,
73-
) -> Result<LightClient, BuilderError>;
67+
) -> Result<LightClient<Idle>, BuilderError>;
7468
}
7569

7670
impl BuilderExt for Builder {
7771
fn build_with_wallet(
7872
mut self,
7973
wallet: &Wallet,
8074
scan_type: ScanType,
81-
) -> Result<LightClient, BuilderError> {
75+
) -> Result<LightClient<Idle>, BuilderError> {
8276
let network = wallet.network();
8377
if self.network().ne(&network) {
8478
return Err(BuilderError::NetworkMismatch);
@@ -109,13 +103,16 @@ impl BuilderExt for Builder {
109103
wallet.latest_checkpoint(),
110104
indexed_graph,
111105
);
112-
Ok(LightClient {
106+
let client = LightClient::new(
113107
requester,
114-
info_subscriber: info_rx,
115-
warning_subscriber: warn_rx,
108+
LoggingSubscribers {
109+
info_subscriber: info_rx,
110+
warning_subscriber: warn_rx,
111+
},
116112
update_subscriber,
117113
node,
118-
})
114+
);
115+
Ok(client)
119116
}
120117
}
121118

src/lib.rs

Lines changed: 149 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,9 @@
1919
//! .network(Network::Signet)
2020
//! .create_wallet_no_persist()?;
2121
//!
22-
//! let LightClient {
23-
//! requester,
24-
//! info_subscriber: _,
25-
//! warning_subscriber: _,
26-
//! mut update_subscriber,
27-
//! node
28-
//! } = Builder::new(Network::Signet).build_with_wallet(&wallet, ScanType::Sync)?;
29-
//!
30-
//! tokio::task::spawn(async move { node.run().await });
22+
//! let client = Builder::new(Network::Signet).build_with_wallet(&wallet, ScanType::Sync)?;
23+
//! let (client, _, mut update_subscriber) = client.subscribe();
24+
//! client.start();
3125
//!
3226
//! loop {
3327
//! let update = update_subscriber.update().await?;
@@ -51,6 +45,7 @@ use bdk_wallet::KeychainKind;
5145
pub extern crate bip157;
5246

5347
use bip157::chain::BlockHeaderChanges;
48+
use bip157::tokio;
5449
use bip157::ScriptBuf;
5550
#[doc(inline)]
5651
pub use bip157::{
@@ -68,19 +63,156 @@ pub use bip157::UnboundedReceiver;
6863
pub use builder::BuilderExt;
6964
pub mod builder;
7065

66+
/// Client state when idle.
67+
pub struct Idle;
68+
/// Client state when subscribed to events.
69+
pub struct Subscribed;
70+
/// Client state when active.
71+
pub struct Active;
72+
73+
mod sealed {
74+
pub trait Sealed {}
75+
}
76+
77+
impl sealed::Sealed for Idle {}
78+
impl sealed::Sealed for Subscribed {}
79+
impl sealed::Sealed for Active {}
80+
81+
/// State of the client.
82+
pub trait State: sealed::Sealed {}
83+
84+
impl State for Idle {}
85+
impl State for Subscribed {}
86+
impl State for Active {}
87+
88+
/// Subscribe to events, notably
7189
#[derive(Debug)]
72-
/// A node and associated structs to send and receive events to and from the node.
73-
pub struct LightClient {
74-
/// Send events to a running node (i.e. broadcast a transaction).
75-
pub requester: Requester,
90+
pub struct LoggingSubscribers {
7691
/// Receive informational messages as the node runs.
7792
pub info_subscriber: Receiver<Info>,
7893
/// Receive warnings from the node as it runs.
7994
pub warning_subscriber: UnboundedReceiver<Warning>,
80-
/// Receive wallet updates from a node.
81-
pub update_subscriber: UpdateSubscriber,
82-
/// The underlying node that must be run to fetch blocks from peers.
83-
pub node: Node,
95+
}
96+
97+
#[derive(Debug)]
98+
/// A client and associated structs to send and receive events to and from a node process.
99+
///
100+
/// The client has three states:
101+
/// - [`Idle`]: the client has been initialized.
102+
/// - [`Subscribed`]: the application is ready to handle logs and updates, but the process is not
103+
/// running yet
104+
/// - [`Active`]: the client is actively fetching data and may now handle requests.
105+
pub struct LightClient<S: State> {
106+
// Send events to a running node (i.e. broadcast a transaction).
107+
requester: Requester,
108+
// Receive info/warnings from the node as it runs.
109+
logging_subscribers: Option<LoggingSubscribers>,
110+
// Receive wallet updates from a node.
111+
update_subscriber: Option<UpdateSubscriber>,
112+
// The underlying node that must be run to fetch blocks from peers.
113+
node: Option<Node>,
114+
_marker: core::marker::PhantomData<S>,
115+
}
116+
117+
impl LightClient<Idle> {
118+
fn new(
119+
requester: Requester,
120+
logging: LoggingSubscribers,
121+
update: UpdateSubscriber,
122+
node: bip157::Node,
123+
) -> LightClient<Idle> {
124+
LightClient {
125+
requester,
126+
logging_subscribers: Some(logging),
127+
update_subscriber: Some(update),
128+
node: Some(node),
129+
_marker: core::marker::PhantomData,
130+
}
131+
}
132+
133+
/// Subscribe to events emitted by the underlying data fetching process. This includes logging
134+
/// and wallet updates. During this step, one may start threads that log to a file and apply
135+
/// updates to a wallet.
136+
///
137+
/// # Returns
138+
///
139+
/// - [`LightClient<Subscribed>`], a client ready to start.
140+
/// - [`LoggingSubscribers`], info and warning messages to display to a user or write to file.
141+
/// - [`UpdateSubscriber`], used to await updates related to the user's wallet.
142+
pub fn subscribe(
143+
mut self,
144+
) -> (
145+
LightClient<Subscribed>,
146+
LoggingSubscribers,
147+
UpdateSubscriber,
148+
) {
149+
let logging =
150+
core::mem::take(&mut self.logging_subscribers).expect("cannot subscribe twice.");
151+
let updates =
152+
core::mem::take(&mut self.update_subscriber).expect("cannot subscribe twice.");
153+
let client = LightClient {
154+
requester: self.requester,
155+
logging_subscribers: None,
156+
update_subscriber: None,
157+
node: self.node,
158+
_marker: core::marker::PhantomData,
159+
};
160+
(client, logging, updates)
161+
}
162+
}
163+
164+
impl LightClient<Subscribed> {
165+
/// Start fetching data for the wallet on a dedicated [`tokio::task`]. This will continually
166+
/// run until terminated or no peers could be found.
167+
///
168+
/// # Panics
169+
///
170+
/// If there is no [`tokio::runtime::Runtime`] to drive execution. Common in synchronous
171+
/// setups.
172+
pub fn start(mut self) -> LightClient<Active> {
173+
let node = core::mem::take(&mut self.node).expect("cannot start twice.");
174+
tokio::task::spawn(async move { node.run().await });
175+
LightClient {
176+
requester: self.requester,
177+
logging_subscribers: None,
178+
update_subscriber: None,
179+
node: None,
180+
_marker: core::marker::PhantomData,
181+
}
182+
}
183+
184+
/// Take the underlying node process to run in a custom way. Examples include using a dedicated
185+
/// [`tokio::runtime::Runtime`] or [`tokio::runtime::Handle`] to drive execution.
186+
pub fn managed_start(mut self) -> (LightClient<Active>, Node) {
187+
let node = core::mem::take(&mut self.node).expect("cannot start twice.");
188+
let client = LightClient {
189+
requester: self.requester,
190+
logging_subscribers: None,
191+
update_subscriber: None,
192+
node: None,
193+
_marker: core::marker::PhantomData,
194+
};
195+
(client, node)
196+
}
197+
}
198+
199+
impl LightClient<Active> {
200+
/// The client is active and may now handle requests with a [`Requester`].
201+
pub fn requester(self) -> Requester {
202+
self.requester
203+
}
204+
}
205+
206+
impl From<LightClient<Active>> for Requester {
207+
fn from(value: LightClient<Active>) -> Self {
208+
value.requester
209+
}
210+
}
211+
212+
impl AsRef<Requester> for LightClient<Active> {
213+
fn as_ref(&self) -> &Requester {
214+
&self.requester
215+
}
84216
}
85217

86218
/// Interpret events from a node that is running to apply

0 commit comments

Comments
 (0)