1use crate::advertiser::Advertiser;
7use crate::broadcaster::{ArcBroadcaster, Broadcaster};
8use crate::engine::{Engine, EngineConfig};
9use crate::lookup_service::LookupService;
10use crate::storage::Storage;
11use crate::topic_manager::TopicManager;
12use std::collections::HashMap;
13
14pub struct EngineBuilder {
26 storage: Box<dyn Storage>,
27 managers: HashMap<String, Box<dyn TopicManager>>,
28 lookup_services: HashMap<String, Box<dyn LookupService>>,
29 advertiser: Option<Box<dyn Advertiser>>,
30 broadcaster: Option<Box<dyn Broadcaster>>,
31 arc_broadcaster: Option<Box<dyn ArcBroadcaster>>,
32 chain_tracker: Option<Box<dyn bsv_rs::transaction::ChainTracker>>,
33 hosting_url: Option<String>,
34 ship_trackers: Vec<String>,
35 slap_trackers: Vec<String>,
36 suppress_default_sync_ads: bool,
37 verify_scripts: bool,
38 walk_budget: crate::engine::DoorBudget,
39}
40
41impl EngineBuilder {
42 pub fn new(storage: Box<dyn Storage>) -> Self {
44 Self {
45 storage,
46 managers: HashMap::new(),
47 lookup_services: HashMap::new(),
48 advertiser: None,
49 broadcaster: None,
50 arc_broadcaster: None,
51 chain_tracker: None,
52 hosting_url: None,
53 ship_trackers: Vec::new(),
54 slap_trackers: Vec::new(),
55 suppress_default_sync_ads: true,
56 verify_scripts: true,
57 walk_budget: crate::engine::DoorBudget::DEFAULT,
58 }
59 }
60
61 pub fn with_topic(mut self, name: impl Into<String>, manager: Box<dyn TopicManager>) -> Self {
63 self.managers.insert(name.into(), manager);
64 self
65 }
66
67 pub fn with_lookup(mut self, name: impl Into<String>, service: Box<dyn LookupService>) -> Self {
69 self.lookup_services.insert(name.into(), service);
70 self
71 }
72
73 pub fn with_advertiser(mut self, advertiser: Box<dyn Advertiser>) -> Self {
75 self.advertiser = Some(advertiser);
76 self
77 }
78
79 pub fn with_broadcaster(mut self, broadcaster: Box<dyn Broadcaster>) -> Self {
81 self.broadcaster = Some(broadcaster);
82 self
83 }
84
85 pub fn with_arc_broadcaster(mut self, arc: Box<dyn ArcBroadcaster>) -> Self {
87 self.arc_broadcaster = Some(arc);
88 self
89 }
90
91 pub fn with_chain_tracker(
93 mut self,
94 tracker: Box<dyn bsv_rs::transaction::ChainTracker>,
95 ) -> Self {
96 self.chain_tracker = Some(tracker);
97 self
98 }
99
100 pub fn with_hosting_url(mut self, url: impl Into<String>) -> Self {
102 self.hosting_url = Some(url.into());
103 self
104 }
105
106 pub fn with_ship_trackers(mut self, trackers: Vec<String>) -> Self {
108 self.ship_trackers = trackers;
109 self
110 }
111
112 pub fn with_slap_trackers(mut self, trackers: Vec<String>) -> Self {
114 self.slap_trackers = trackers;
115 self
116 }
117
118 pub fn suppress_default_sync_advertisements(mut self, suppress: bool) -> Self {
120 self.suppress_default_sync_ads = suppress;
121 self
122 }
123
124 pub fn with_script_verification(mut self, enabled: bool) -> Self {
139 self.verify_scripts = enabled;
140 self
141 }
142
143 pub fn with_walk_budget(mut self, budget: crate::engine::DoorBudget) -> Self {
147 self.walk_budget = budget;
148 self
149 }
150
151 pub fn build(self) -> Engine {
153 let mut engine = Engine::with_all(
154 self.managers,
155 self.lookup_services,
156 self.storage,
157 self.advertiser,
158 self.broadcaster,
159 self.arc_broadcaster,
160 self.chain_tracker,
161 EngineConfig {
162 hosting_url: self.hosting_url,
163 ship_trackers: self.ship_trackers,
164 slap_trackers: self.slap_trackers,
165 sync_configuration: HashMap::new(),
166 suppress_default_sync_advertisements: self.suppress_default_sync_ads,
167 },
168 );
169 engine.set_script_verification(self.verify_scripts);
170 engine.set_walk_budget(self.walk_budget);
171 engine
172 }
173}
174
175#[cfg(test)]
180mod tests {
181 use super::*;
182 use crate::storage::memory::MemoryStorage;
183 use crate::topic_manager::TopicManagerError;
184 use crate::types::*;
185 use async_trait::async_trait;
186
187 struct DummyTM;
188 #[async_trait(?Send)]
189 impl TopicManager for DummyTM {
190 async fn identify_admissible_outputs(
191 &self,
192 _: &bsv_rs::transaction::Transaction,
193 _: &[u8],
194 _: Option<&[u8]>,
195 _: SubmitMode,
196 _context: &TopicAdmittanceContext,
197 ) -> Result<AdmittanceInstructions, TopicManagerError> {
198 Ok(AdmittanceInstructions::default())
199 }
200 async fn get_documentation(&self) -> String {
201 "Dummy".into()
202 }
203 async fn get_metadata(&self) -> ServiceMetadata {
204 ServiceMetadata {
205 name: "dummy".into(),
206 ..Default::default()
207 }
208 }
209 }
210
211 struct DummyLS;
212 #[async_trait(?Send)]
213 impl LookupService for DummyLS {
214 fn admission_mode(&self) -> AdmissionMode {
215 AdmissionMode::LockingScript
216 }
217 fn spend_notification_mode(&self) -> SpendNotificationMode {
218 SpendNotificationMode::None
219 }
220 async fn output_admitted_by_topic(
221 &self,
222 _: &OutputAdmittedByTopic,
223 ) -> Result<(), crate::lookup_service::LookupServiceError> {
224 Ok(())
225 }
226 async fn output_evicted(
227 &self,
228 _: &str,
229 _: u32,
230 ) -> Result<(), crate::lookup_service::LookupServiceError> {
231 Ok(())
232 }
233 async fn lookup(
234 &self,
235 _: &LookupQuestion,
236 ) -> Result<LookupResult, crate::lookup_service::LookupServiceError> {
237 Ok(LookupResult::OutputList(vec![]))
238 }
239 async fn get_documentation(&self) -> String {
240 "Dummy".into()
241 }
242 async fn get_metadata(&self) -> ServiceMetadata {
243 ServiceMetadata {
244 name: "dummy-ls".into(),
245 ..Default::default()
246 }
247 }
248 }
249
250 #[tokio::test]
251 async fn builder_creates_engine_with_topics() {
252 let engine = EngineBuilder::new(Box::new(MemoryStorage::new()))
253 .with_topic("tm_test", Box::new(DummyTM))
254 .with_lookup("ls_test", Box::new(DummyLS))
255 .build();
256
257 let managers = engine.list_topic_managers().await;
258 assert!(managers.contains_key("tm_test"));
259 assert_eq!(managers["tm_test"].name, "dummy");
260
261 let services = engine.list_lookup_service_providers().await;
262 assert!(services.contains_key("ls_test"));
263 }
264
265 #[tokio::test]
266 async fn builder_with_multiple_topics() {
267 let engine = EngineBuilder::new(Box::new(MemoryStorage::new()))
268 .with_topic("tm_alpha", Box::new(DummyTM))
269 .with_topic("tm_beta", Box::new(DummyTM))
270 .with_lookup("ls_alpha", Box::new(DummyLS))
271 .with_lookup("ls_beta", Box::new(DummyLS))
272 .build();
273
274 let managers = engine.list_topic_managers().await;
275 assert_eq!(managers.len(), 2);
276 }
277
278 #[test]
279 fn builder_with_hosting_url() {
280 let engine = EngineBuilder::new(Box::new(MemoryStorage::new()))
281 .with_hosting_url("https://overlay.example.com")
282 .build();
283
284 assert_eq!(
285 engine.config().hosting_url.as_deref(),
286 Some("https://overlay.example.com")
287 );
288 }
289
290 #[test]
291 fn builder_with_trackers() {
292 let engine = EngineBuilder::new(Box::new(MemoryStorage::new()))
293 .with_ship_trackers(vec!["https://ship1.example.com".into()])
294 .with_slap_trackers(vec!["https://slap1.example.com".into()])
295 .build();
296
297 assert_eq!(engine.config().ship_trackers.len(), 1);
298 assert_eq!(engine.config().slap_trackers.len(), 1);
299 }
300
301 #[test]
302 fn builder_empty_is_valid() {
303 let engine = EngineBuilder::new(Box::new(MemoryStorage::new())).build();
304 assert!(engine.config().hosting_url.is_none());
305 }
306
307 #[test]
308 fn builder_script_verification_defaults_on_and_can_be_switched_off() {
309 let engine = EngineBuilder::new(Box::new(MemoryStorage::new())).build();
310 assert!(
311 engine.script_verification(),
312 "reference parity: scripts are verified on submit by default"
313 );
314
315 let engine = EngineBuilder::new(Box::new(MemoryStorage::new()))
316 .with_script_verification(false)
317 .build();
318 assert!(
319 !engine.script_verification(),
320 "the escape hatch turns it off"
321 );
322 }
323
324 #[test]
325 fn builder_sync_config_defaults() {
326 let engine = EngineBuilder::new(Box::new(MemoryStorage::new()))
327 .with_topic("tm_custom", Box::new(DummyTM))
328 .build();
329
330 assert!(engine.config().sync_configuration.contains_key("tm_custom"));
332 }
333}