1pub mod bus;
34pub mod cache;
35pub mod compression;
36pub mod config;
37pub mod health;
38mod http;
39pub mod http_health;
40pub mod http_server;
41pub mod materialized_view;
42#[cfg(feature = "otel")]
43pub mod metrics;
44pub mod mutation_batch;
45pub mod program_runtime;
46pub mod projector;
47pub mod runtime;
48pub mod snapshot;
49pub mod sorted_cache;
50pub mod telemetry;
51pub mod view;
52pub mod websocket;
53
54pub use arete_auth::{
55 AsyncVerifier, KeyLoader, Limits, SolanaGatewayAuthorization, SolanaGatewayAuthorizationError,
56 SolanaGatewayScope, TargetKind, TokenVerifier, VerifyingKey, SCOPE_READ,
57 SCOPE_TRANSACTION_INSPECT, SCOPE_TRANSACTION_SEND, SOLANA_GATEWAY_AUDIENCE,
58};
59pub use bus::{BusManager, BusMessage};
60pub use cache::{EntityCache, EntityCacheConfig};
61pub use config::{
62 HealthConfig, HttpHealthConfig, HttpServerConfig, ReconnectionConfig, RuntimePlan,
63 ServerConfig, TransactionConfig, WebSocketConfig, YellowstoneConfig,
64};
65pub use health::{HealthMonitor, SlotTracker, StreamStatus};
66pub use http_health::HttpHealthServer;
67pub use http_server::HttpServer;
68pub use materialized_view::{MaterializedView, MaterializedViewRegistry, ViewEffect};
69#[cfg(feature = "otel")]
70pub use metrics::Metrics;
71pub use mutation_batch::{EventContext, MutationBatch, SlotContext};
72pub use program_runtime::{
73 IdlContentHash, NormalizedIdlHash, ProgramAccountReaderFn, ProgramReleaseHash,
74 ProgramRuntimeCatalog, ProgramRuntimeDefinition, ProgramSpecHash,
75};
76pub use projector::Projector;
77pub use runtime::Runtime;
78pub use snapshot::{SnapshotConfig, SnapshotService};
79pub use telemetry::{init as init_telemetry, TelemetryConfig};
80#[cfg(feature = "otel")]
81pub use telemetry::{init_with_otel, TelemetryGuard};
82pub use view::{Delivery, Filters, Projection, ViewIndex, ViewSpec};
83pub use websocket::{
84 AllowAllAuthPlugin, AuthContext, AuthDecision, AuthDeny, AuthErrorDetails, ChannelUsageEmitter,
85 ClientInfo, ClientManager, ConnectionAuthRequest, ErrorResponse, Frame, HttpUsageEmitter, Mode,
86 RateLimitConfig, RateLimitResult, RateLimiterConfig, RefreshAuthRequest, RefreshAuthResponse,
87 RetryPolicy, SignedSessionAuthPlugin, SnapshotOptions, SocketIssueMessage,
88 StaticTokenAuthPlugin, Subscription, SubscriptionQuery, WebSocketAuthPlugin,
89 WebSocketRateLimiter, WebSocketServer, WebSocketUsageBatch, WebSocketUsageEmitter,
90 WebSocketUsageEnvelope, WebSocketUsageEvent,
91};
92
93use anyhow::Result;
94use arete_interpreter::ast::ViewDef;
95use std::net::SocketAddr;
96use std::sync::Arc;
97
98pub type ParserSetupFn = Arc<
100 dyn Fn(
101 tokio::sync::mpsc::Sender<MutationBatch>,
102 Option<HealthMonitor>,
103 ReconnectionConfig,
104 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send>>
105 + Send
106 + Sync,
107>;
108
109pub struct Spec {
112 pub bytecode: arete_interpreter::compiler::MultiEntityBytecode,
113 pub program_ids: Vec<String>,
114 pub parser_setup: Option<ParserSetupFn>,
115 pub program_runtime_definitions: Vec<ProgramRuntimeDefinition>,
116 pub entity_specs: Vec<arete_interpreter::ast::SerializableStreamSpec>,
117 pub views: Vec<ViewDef>,
118}
119
120impl Spec {
121 pub fn new(
122 bytecode: arete_interpreter::compiler::MultiEntityBytecode,
123 program_id: impl Into<String>,
124 ) -> Self {
125 Self {
126 bytecode,
127 program_ids: vec![program_id.into()],
128 parser_setup: None,
129 program_runtime_definitions: Vec::new(),
130 entity_specs: Vec::new(),
131 views: Vec::new(),
132 }
133 }
134
135 pub fn with_parser_setup(mut self, setup_fn: ParserSetupFn) -> Self {
136 self.parser_setup = Some(setup_fn);
137 self
138 }
139
140 pub fn with_program_runtime_definitions(
141 mut self,
142 definitions: Vec<ProgramRuntimeDefinition>,
143 ) -> Self {
144 for definition in &definitions {
145 if !self.program_ids.contains(&definition.program_id) {
146 self.program_ids.push(definition.program_id.clone());
147 }
148 }
149 self.program_runtime_definitions = definitions;
150 self
151 }
152
153 pub fn with_entity_specs(
154 mut self,
155 entity_specs: Vec<arete_interpreter::ast::SerializableStreamSpec>,
156 ) -> Self {
157 self.entity_specs = entity_specs;
158 self
159 }
160
161 pub fn with_views(mut self, views: Vec<ViewDef>) -> Self {
162 self.views = views;
163 self
164 }
165}
166
167pub struct Server;
169
170impl Server {
171 pub fn builder() -> ServerBuilder {
173 ServerBuilder::new()
174 }
175
176 pub fn solana_gateway(target_id: impl Into<String>) -> SolanaGatewayBuilder {
178 SolanaGatewayBuilder::new(target_id.into())
179 }
180}
181
182pub struct SolanaGatewayBuilder {
188 inner: ServerBuilder,
189}
190
191impl SolanaGatewayBuilder {
192 fn new(target_id: String) -> Self {
193 let mut inner = ServerBuilder::new();
194 inner.config.http_health = Some(HttpHealthConfig::default());
195 inner.config.runtime_plan = RuntimePlan::solana_gateway();
196 inner.config.solana_gateway_target_id = Some(target_id);
197 Self { inner }
198 }
199
200 pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
202 self.inner.config.http_health = Some(HttpHealthConfig::new(addr));
203 self
204 }
205
206 pub fn auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
208 self.inner.http_auth_plugin = Some(plugin);
209 self
210 }
211
212 pub fn transactions_config(mut self, config: TransactionConfig) -> Self {
214 self.inner.config.transactions = Some(config);
215 self
216 }
217
218 fn finalize(mut self) -> Result<ServerBuilder> {
219 if self
220 .inner
221 .config
222 .solana_gateway_target_id
223 .as_deref()
224 .is_none_or(|target_id| target_id.trim().is_empty())
225 {
226 anyhow::bail!("the Solana gateway target ID must not be empty");
227 }
228 if let Some(config) = self.inner.config.transactions.as_ref() {
229 config.validate()?;
230 if !config.enabled {
231 anyhow::bail!("Solana gateway transaction configuration must be enabled");
232 }
233 }
234 self.inner.config.runtime_plan = RuntimePlan::solana_gateway();
235 Ok(self.inner)
236 }
237
238 pub fn build(self) -> Result<Runtime> {
240 self.finalize()?.build()
241 }
242
243 pub async fn start(self) -> Result<()> {
245 self.finalize()?.start().await
246 }
247}
248
249pub struct ServerBuilder {
251 spec: Option<Spec>,
252 views: Option<ViewIndex>,
253 materialized_views: Option<MaterializedViewRegistry>,
254 config: ServerConfig,
255 websocket_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
256 http_auth_plugin: Option<Arc<dyn WebSocketAuthPlugin>>,
257 websocket_usage_emitter: Option<Arc<dyn WebSocketUsageEmitter>>,
258 websocket_max_clients: Option<usize>,
259 websocket_rate_limit_config: Option<crate::websocket::client_manager::RateLimitConfig>,
260 #[cfg(feature = "otel")]
261 metrics: Option<Arc<Metrics>>,
262}
263
264impl ServerBuilder {
265 fn new() -> Self {
266 Self {
267 spec: None,
268 views: None,
269 materialized_views: None,
270 config: ServerConfig::new(),
271 websocket_auth_plugin: None,
272 http_auth_plugin: None,
273 websocket_usage_emitter: None,
274 websocket_max_clients: None,
275 websocket_rate_limit_config: None,
276 #[cfg(feature = "otel")]
277 metrics: None,
278 }
279 }
280
281 pub fn spec(mut self, spec: Spec) -> Self {
283 self.spec = Some(spec);
284 self
285 }
286
287 pub fn views(mut self, views: ViewIndex) -> Self {
289 self.views = Some(views);
290 self
291 }
292
293 #[cfg(feature = "otel")]
295 pub fn metrics(mut self, metrics: Metrics) -> Self {
296 self.metrics = Some(Arc::new(metrics));
297 self
298 }
299
300 pub fn websocket(mut self) -> Self {
302 self.config.websocket = Some(WebSocketConfig::default());
303 self.config.runtime_plan.websocket = true;
304 self.config.runtime_plan.live_runtime = true;
305 self
306 }
307
308 pub fn websocket_config(mut self, config: WebSocketConfig) -> Self {
310 self.config.websocket = Some(config);
311 self.config.runtime_plan.websocket = true;
312 self.config.runtime_plan.live_runtime = true;
313 self
314 }
315
316 pub fn websocket_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
318 self.websocket_auth_plugin = Some(plugin);
319 self
320 }
321
322 pub fn http_auth_plugin(mut self, plugin: Arc<dyn WebSocketAuthPlugin>) -> Self {
324 self.http_auth_plugin = Some(plugin);
325 self
326 }
327
328 pub fn websocket_usage_emitter(mut self, emitter: Arc<dyn WebSocketUsageEmitter>) -> Self {
330 self.websocket_usage_emitter = Some(emitter);
331 self
332 }
333
334 pub fn websocket_max_clients(mut self, max_clients: usize) -> Self {
336 self.websocket_max_clients = Some(max_clients);
337 self
338 }
339
340 pub fn websocket_rate_limit_config(
346 mut self,
347 config: crate::websocket::client_manager::RateLimitConfig,
348 ) -> Self {
349 self.websocket_rate_limit_config = Some(config);
350 self
351 }
352
353 pub fn bind(mut self, addr: impl Into<SocketAddr>) -> Self {
355 if let Some(ws_config) = &mut self.config.websocket {
356 ws_config.bind_address = addr.into();
357 } else {
358 self.config.websocket = Some(WebSocketConfig::new(addr.into()));
359 }
360 self.config.runtime_plan.websocket = true;
361 self.config.runtime_plan.live_runtime = true;
362 self
363 }
364
365 pub fn yellowstone(mut self, config: YellowstoneConfig) -> Self {
367 self.config.yellowstone = Some(config);
368 self.config.runtime_plan.live_runtime = true;
369 self
370 }
371
372 pub fn health_monitoring(mut self) -> Self {
374 self.config.health = Some(HealthConfig::default());
375 self.config.runtime_plan.health = true;
376 self
377 }
378
379 pub fn health_config(mut self, config: HealthConfig) -> Self {
381 self.config.health = Some(config);
382 self.config.runtime_plan.health = true;
383 self
384 }
385
386 pub fn snapshots(mut self, config: crate::snapshot::SnapshotConfig) -> Self {
389 self.config.snapshots = Some(config);
390 self
391 }
392
393 pub fn reconnection(mut self) -> Self {
395 self.config.reconnection = Some(ReconnectionConfig::default());
396 self
397 }
398
399 pub fn reconnection_config(mut self, config: ReconnectionConfig) -> Self {
401 self.config.reconnection = Some(config);
402 self
403 }
404
405 pub fn http(mut self) -> Self {
409 self.config.http_health = Some(HttpHealthConfig::default());
410 self.config.runtime_plan.health = true;
411 self.config.runtime_plan.chain_reads = true;
412 self.config.runtime_plan.program_reads = true;
413 self.config.runtime_plan.stack_queries = true;
414 self
415 }
416
417 pub fn http_config(mut self, config: crate::http_server::HttpServerConfig) -> Self {
419 self.config.http_health = Some(config);
420 self.config.runtime_plan.health = true;
421 self.config.runtime_plan.chain_reads = true;
422 self.config.runtime_plan.program_reads = true;
423 self.config.runtime_plan.stack_queries = true;
424 self
425 }
426
427 pub fn transactions_config(mut self, config: TransactionConfig) -> Self {
429 self.config.runtime_plan.transactions = config.enabled;
430 self.config.transactions = Some(config);
431 self
432 }
433
434 pub fn runtime_plan(mut self, plan: RuntimePlan) -> Self {
436 self.config.runtime_plan = plan;
437 self
438 }
439
440 pub fn program_reads(mut self) -> Self {
442 if self.config.http_health.is_none() {
443 self.config.http_health = Some(HttpHealthConfig::default());
444 }
445 self.config.runtime_plan.health = true;
446 self.config.runtime_plan.program_reads = true;
447 self
448 }
449
450 pub fn program_read_binding(mut self, target_id: impl Into<String>) -> Self {
452 if self.config.http_health.is_none() {
453 self.config.http_health = Some(HttpHealthConfig::default());
454 }
455 self.config.runtime_plan.health = true;
456 self.config.runtime_plan.program_reads = true;
457 self.config.program_read_binding_target_id = Some(target_id.into());
458 self
459 }
460
461 pub fn chain_reads(mut self) -> Self {
462 if self.config.http_health.is_none() {
463 self.config.http_health = Some(HttpHealthConfig::default());
464 }
465 self.config.runtime_plan.chain_reads = true;
466 self
467 }
468
469 pub fn stack_queries(mut self) -> Self {
470 if self.config.http_health.is_none() {
471 self.config.http_health = Some(HttpHealthConfig::default());
472 }
473 self.config.runtime_plan.stack_queries = true;
474 self
475 }
476
477 pub fn live_runtime(mut self) -> Self {
478 self.config.runtime_plan.live_runtime = true;
479 self
480 }
481
482 pub fn http_bind(mut self, addr: impl Into<SocketAddr>) -> Self {
484 if let Some(http_config) = &mut self.config.http_health {
485 http_config.bind_address = addr.into();
486 } else {
487 self.config.http_health = Some(HttpHealthConfig::new(addr.into()));
488 }
489 self.config.runtime_plan.health = true;
490 self.config.runtime_plan.chain_reads = true;
491 self.config.runtime_plan.program_reads = true;
492 self.config.runtime_plan.stack_queries = true;
493 self
494 }
495
496 pub fn http_health(self) -> Self {
498 self.http()
499 }
500
501 pub fn http_health_config(self, config: HttpHealthConfig) -> Self {
503 self.http_config(config)
504 }
505
506 pub fn health_bind(self, addr: impl Into<SocketAddr>) -> Self {
508 self.http_bind(addr)
509 }
510
511 pub async fn start(self) -> Result<()> {
512 let (view_index, materialized_registry) =
513 Self::build_view_index_and_registry(self.views, self.materialized_views, &self.spec);
514
515 #[cfg(feature = "otel")]
516 let mut runtime = Runtime::new(self.config, view_index, self.metrics);
517 #[cfg(not(feature = "otel"))]
518 let mut runtime = Runtime::new(self.config, view_index);
519
520 if let Some(plugin) = self.websocket_auth_plugin {
521 runtime = runtime.with_websocket_auth_plugin(plugin);
522 }
523
524 if let Some(plugin) = self.http_auth_plugin {
525 runtime = runtime.with_http_auth_plugin(plugin);
526 }
527
528 if let Some(emitter) = self.websocket_usage_emitter {
529 runtime = runtime.with_websocket_usage_emitter(emitter);
530 }
531
532 if let Some(max_clients) = self.websocket_max_clients {
533 runtime = runtime.with_websocket_max_clients(max_clients);
534 }
535
536 if let Some(rate_limit_config) = self.websocket_rate_limit_config {
537 runtime = runtime.with_websocket_rate_limit_config(rate_limit_config);
538 }
539
540 if let Some(registry) = materialized_registry {
541 runtime = runtime.with_materialized_views(registry);
542 }
543
544 if let Some(spec) = self.spec {
545 runtime = runtime.with_spec(spec)?;
546 }
547
548 runtime.run().await
549 }
550
551 fn build_view_index_and_registry(
552 views: Option<ViewIndex>,
553 materialized_views: Option<MaterializedViewRegistry>,
554 spec: &Option<Spec>,
555 ) -> (ViewIndex, Option<MaterializedViewRegistry>) {
556 let mut index = views.unwrap_or_default();
557 let mut registry = materialized_views;
558
559 if let Some(ref spec) = spec {
560 let entity_wire_formats = spec
561 .entity_specs
562 .iter()
563 .map(|entity_spec| {
564 (
565 entity_spec.state_name.clone(),
566 ViewSpec::wire_format_from_entity_spec(entity_spec),
567 )
568 })
569 .collect::<std::collections::HashMap<_, _>>();
570
571 for entity_name in spec.bytecode.entities.keys() {
572 let wire_format = entity_wire_formats
573 .get(entity_name)
574 .cloned()
575 .unwrap_or_default();
576 index.add_spec(ViewSpec {
577 id: format!("{}/list", entity_name),
578 export: entity_name.clone(),
579 mode: Mode::List,
580 wire_format: wire_format.clone(),
581 projection: Projection::all(),
582 filters: Filters::all(),
583 delivery: Delivery::default(),
584 pipeline: None,
585 source_view: None,
586 });
587
588 index.add_spec(ViewSpec {
589 id: format!("{}/state", entity_name),
590 export: entity_name.clone(),
591 mode: Mode::State,
592 wire_format: wire_format.clone(),
593 projection: Projection::all(),
594 filters: Filters::all(),
595 delivery: Delivery::default(),
596 pipeline: None,
597 source_view: None,
598 });
599
600 index.add_spec(ViewSpec {
601 id: format!("{}/append", entity_name),
602 export: entity_name.clone(),
603 mode: Mode::Append,
604 wire_format,
605 projection: Projection::all(),
606 filters: Filters::all(),
607 delivery: Delivery::default(),
608 pipeline: None,
609 source_view: None,
610 });
611 }
612
613 if !spec.views.is_empty() {
614 let reg = registry.get_or_insert_with(MaterializedViewRegistry::new);
615
616 for view_def in &spec.views {
617 let export = match &view_def.source {
618 arete_interpreter::ast::ViewSource::Entity { name } => name.clone(),
619 arete_interpreter::ast::ViewSource::View { id } => {
620 id.split('/').next().unwrap_or(id).to_string()
621 }
622 };
623
624 if Self::is_canonical_entity_view(view_def, &export) {
631 tracing::debug!(
632 view_id = %view_def.id,
633 "Keeping canonical entity view backed by the native cache"
634 );
635 continue;
636 }
637
638 let wire_format = entity_wire_formats
639 .get(&export)
640 .cloned()
641 .unwrap_or_default();
642 let view_spec = ViewSpec::from_view_def(view_def, &export, wire_format);
643 let pipeline = view_spec.pipeline.clone().unwrap_or_default();
644 let source_id = view_spec.source_view.clone().unwrap_or_default();
645 tracing::debug!(
646 view_id = %view_def.id,
647 source = %source_id,
648 "Registering derived view"
649 );
650
651 index.add_spec(view_spec);
652
653 let materialized =
654 MaterializedView::new(view_def.id.clone(), source_id, pipeline);
655 reg.register(materialized);
656 }
657 }
658 }
659
660 (index, registry)
661 }
662
663 fn is_canonical_entity_view(view_def: &ViewDef, export: &str) -> bool {
664 use arete_interpreter::ast::{ViewOutput, ViewSource};
665
666 let ViewSource::Entity { name } = &view_def.source else {
667 return false;
668 };
669 if name != export || !view_def.pipeline.is_empty() {
670 return false;
671 }
672
673 match &view_def.output {
674 ViewOutput::Collection => view_def.id == format!("{export}/list"),
675 ViewOutput::Single | ViewOutput::Keyed { .. } => {
676 view_def.id == format!("{export}/state")
677 }
678 }
679 }
680
681 pub fn build(self) -> Result<Runtime> {
682 let (view_index, materialized_registry) =
683 Self::build_view_index_and_registry(self.views, self.materialized_views, &self.spec);
684
685 #[cfg(feature = "otel")]
686 let mut runtime = Runtime::new(self.config, view_index, self.metrics);
687 #[cfg(not(feature = "otel"))]
688 let mut runtime = Runtime::new(self.config, view_index);
689
690 if let Some(plugin) = self.websocket_auth_plugin {
691 runtime = runtime.with_websocket_auth_plugin(plugin);
692 }
693
694 if let Some(plugin) = self.http_auth_plugin {
695 runtime = runtime.with_http_auth_plugin(plugin);
696 }
697
698 if let Some(max_clients) = self.websocket_max_clients {
699 runtime = runtime.with_websocket_max_clients(max_clients);
700 }
701
702 if let Some(registry) = materialized_registry {
703 runtime = runtime.with_materialized_views(registry);
704 }
705
706 if let Some(spec) = self.spec {
707 runtime = runtime.with_spec(spec)?;
708 }
709 Ok(runtime)
710 }
711}
712
713#[cfg(test)]
714mod tests {
715 use super::*;
716
717 #[test]
718 fn test_builder_pattern() {
719 let _builder = Server::builder()
720 .websocket()
721 .bind("[::]:8877".parse::<SocketAddr>().unwrap());
722 }
723
724 #[test]
725 fn test_spec_creation() {
726 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new().build();
727 let spec = Spec::new(bytecode, "test_program");
728 assert_eq!(
729 spec.program_ids.first().map(String::as_str),
730 Some("test_program")
731 );
732 }
733
734 #[test]
735 fn http_without_websocket_has_a_read_only_runtime_plan() {
736 let builder = Server::builder().http();
737 assert!(builder.config.runtime_plan.program_reads);
738 assert!(!builder.config.runtime_plan.live_runtime_enabled());
739 }
740
741 #[test]
742 fn program_read_binding_configures_the_exact_auth_target() {
743 let builder = Server::builder().program_read_binding("binding-1");
744
745 assert!(builder.config.runtime_plan.program_reads);
746 assert_eq!(
747 builder.config.program_read_binding_target_id.as_deref(),
748 Some("binding-1")
749 );
750 }
751
752 #[test]
753 fn websocket_and_http_preserve_all_in_one_runtime_behavior() {
754 let builder = Server::builder().websocket().http();
755 assert!(builder.config.runtime_plan.websocket);
756 assert!(builder.config.runtime_plan.live_runtime_enabled());
757 }
758
759 #[test]
760 fn explicit_hosted_plan_disables_program_reads_after_http_helpers() {
761 let plan = RuntimePlan {
762 health: true,
763 chain_reads: true,
764 program_reads: false,
765 stack_queries: true,
766 transactions: true,
767 websocket: true,
768 live_runtime: true,
769 };
770 let builder = Server::builder()
771 .websocket()
772 .http_health()
773 .health_bind("[::]:8081".parse::<SocketAddr>().unwrap())
774 .runtime_plan(plan);
775
776 assert_eq!(builder.config.runtime_plan, plan);
777 }
778
779 #[test]
780 fn solana_gateway_builder_excludes_stack_and_live_capabilities() {
781 let builder = Server::solana_gateway("gateway-us-east-1");
782
783 assert_eq!(
784 builder.inner.config.runtime_plan,
785 RuntimePlan::solana_gateway()
786 );
787 assert!(builder.inner.config.http_health.is_some());
788 assert_eq!(
789 builder.inner.config.solana_gateway_target_id.as_deref(),
790 Some("gateway-us-east-1")
791 );
792 assert!(builder.inner.config.websocket.is_none());
793 assert!(builder.inner.config.yellowstone.is_none());
794 assert!(builder.inner.spec.is_none());
795 assert!(builder.inner.views.is_none());
796 assert!(builder.inner.materialized_views.is_none());
797 assert!(!builder.inner.config.runtime_plan.websocket);
798 assert!(!builder.inner.config.runtime_plan.live_runtime_enabled());
799 assert!(!builder.inner.config.runtime_plan.stack_queries);
800 assert!(!builder.inner.config.runtime_plan.program_reads);
801 }
802
803 #[test]
804 fn solana_gateway_builder_rejects_invalid_gateway_configuration() {
805 assert!(Server::solana_gateway("").build().is_err());
806 assert!(Server::solana_gateway("gateway-us-east-1")
807 .transactions_config(TransactionConfig::default())
808 .build()
809 .is_err());
810 }
811
812 #[test]
813 fn builder_rejects_mismatched_program_release_definitions() {
814 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new().build();
815 let definition = ProgramRuntimeDefinition {
816 program_id: "Program111".to_string(),
817 program_spec_hash: ProgramSpecHash::from_digest([1; 32]),
818 idl_content_hash: IdlContentHash::from_digest([2; 32]),
819 normalized_idl_hash: NormalizedIdlHash::from_digest([3; 32]),
820 program_release_hash: ProgramReleaseHash::from_digest([4; 32]),
821 account_reader: Arc::new(|_, _| Ok(serde_json::Value::Null)),
822 };
823 let spec =
824 Spec::new(bytecode, "Program111").with_program_runtime_definitions(vec![definition]);
825
826 assert!(Server::builder().spec(spec).build().is_err());
827 }
828
829 #[test]
830 fn canonical_entity_views_are_not_reclassified_as_derived_views() {
831 use arete_interpreter::ast::{IdentitySpec, TypedStreamSpec, ViewDef, ViewSource};
832
833 let list = ViewDef::list("OreBoard");
834 let state = ViewDef::state("OreBoard", &["id", "address"]);
835 assert!(ServerBuilder::is_canonical_entity_view(&list, "OreBoard"));
836 assert!(ServerBuilder::is_canonical_entity_view(&state, "OreBoard"));
837
838 let mut named_view = ViewDef::list("OreBoard");
839 named_view.id = "OreBoard/all".to_string();
840 assert!(!ServerBuilder::is_canonical_entity_view(
841 &named_view,
842 "OreBoard"
843 ));
844
845 let mut chained_view = ViewDef::list("OreBoard");
846 chained_view.source = ViewSource::View {
847 id: "OreBoard/list".to_string(),
848 };
849 assert!(!ServerBuilder::is_canonical_entity_view(
850 &chained_view,
851 "OreBoard"
852 ));
853
854 let entity_spec = TypedStreamSpec::<serde_json::Value>::new(
855 "OreBoard".to_string(),
856 IdentitySpec {
857 primary_keys: vec!["id.address".to_string()],
858 lookup_indexes: Vec::new(),
859 },
860 Vec::new(),
861 );
862 let bytecode = arete_interpreter::compiler::MultiEntityBytecode::new()
863 .add_entity("OreBoard".to_string(), entity_spec, 1)
864 .build();
865 let spec = Some(Spec::new(bytecode, "Program111").with_views(vec![list, state]));
866
867 let (index, _) = ServerBuilder::build_view_index_and_registry(None, None, &spec);
868 assert!(!index
869 .get_view("OreBoard/list")
870 .expect("list view should exist")
871 .is_derived());
872 assert!(!index
873 .get_view("OreBoard/state")
874 .expect("state view should exist")
875 .is_derived());
876 assert_eq!(index.by_export("OreBoard").len(), 3);
877 }
878}