1use alloy_primitives::{hex, LogData};
2use iri_string::types::UriString;
3use semver::Version;
4use serde::{Deserialize, Serialize};
5use std::collections::{BTreeMap, BTreeSet};
6use std::num::{NonZeroU32, NonZeroU64};
7use std::str::FromStr;
8use utoipa::ToSchema;
9use wasm_pkg_common::package::PackageRef;
10
11use crate::{ByteArray, ComponentDigest, ServiceDigest, Timestamp};
12
13use super::{ChainKey, ServiceId, WorkflowId};
14
15#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
21#[serde(rename_all = "snake_case")]
22pub struct Service {
23 pub name: String,
25
26 pub workflows: BTreeMap<WorkflowId, Workflow>,
28
29 pub status: ServiceStatus,
30
31 pub manager: ServiceManager,
32}
33
34impl Service {
35 pub fn hash(&self) -> anyhow::Result<ServiceDigest> {
37 let service_bytes = serde_json::to_vec(self)?;
38 Ok(ServiceDigest::hash(&service_bytes))
39 }
40
41 pub fn id(&self) -> ServiceId {
42 ServiceId::from(&self.manager)
43 }
44}
45
46#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema, PartialOrd, Ord)]
47#[serde(rename_all = "snake_case")]
48pub enum ServiceManager {
49 Evm {
50 chain: ChainKey,
51 #[schema(value_type = String)]
52 address: alloy_primitives::Address,
53 },
54}
55
56impl From<&ServiceManager> for ServiceId {
57 fn from(manager: &ServiceManager) -> Self {
58 match manager {
59 ServiceManager::Evm { chain, address } => {
60 let mut bytes = Vec::new();
61 bytes.extend_from_slice(b"evm");
62 bytes.extend_from_slice(chain.to_string().as_bytes());
63 bytes.extend_from_slice(address.as_slice());
64 ServiceId::hash(bytes)
65 }
66 }
67 }
68}
69
70impl ServiceManager {
71 pub fn chain(&self) -> &ChainKey {
72 match self {
73 ServiceManager::Evm { chain, .. } => chain,
74 }
75 }
76
77 pub fn evm_address_unchecked(&self) -> alloy_primitives::Address {
78 match self {
79 ServiceManager::Evm { address, .. } => *address,
80 }
81 }
82}
83
84impl Service {
85 pub fn new_simple(
86 name: Option<String>,
87 trigger: Trigger,
88 source: ComponentSource,
89 submit: Submit,
90 manager: ServiceManager,
91 ) -> Self {
92 let workflow_id = WorkflowId::default();
93
94 let workflow = Workflow {
95 trigger,
96 component: Component::new(source),
97 submit,
98 };
99
100 let workflows = BTreeMap::from([(workflow_id, workflow)]);
101
102 Self {
103 name: name.unwrap_or_else(|| "Unknown".to_string()),
104 workflows,
105 status: ServiceStatus::Active,
106 manager,
107 }
108 }
109}
110
111#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
112#[serde(rename_all = "snake_case")]
113pub struct Component {
114 pub source: ComponentSource,
115
116 pub permissions: Permissions,
119
120 pub fuel_limit: Option<u64>,
123
124 pub time_limit_seconds: Option<u64>,
127
128 pub config: BTreeMap<String, String>,
130
131 pub env_keys: BTreeSet<String>,
134}
135
136#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ToSchema)]
137#[serde(rename_all = "snake_case")]
138pub enum ComponentSource {
139 Download {
141 #[schema(value_type = String)]
142 uri: UriString,
143 digest: ComponentDigest,
144 },
145 Registry {
147 #[serde(flatten)]
148 registry: Registry,
149 },
150 Digest(ComponentDigest),
152}
153
154#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ToSchema)]
155pub struct Registry {
156 pub digest: ComponentDigest,
157 pub domain: Option<String>,
161 #[schema(value_type = Option<String>)]
163 pub version: Option<Version>,
164 #[schema(value_type = String)]
166 pub package: PackageRef,
167}
168
169impl ComponentSource {
170 pub fn digest(&self) -> &ComponentDigest {
171 match self {
172 ComponentSource::Download { digest, .. } => digest,
173 ComponentSource::Registry { registry } => ®istry.digest,
174 ComponentSource::Digest(digest) => digest,
175 }
176 }
177}
178
179#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
182#[serde(rename_all = "snake_case")]
183pub struct Workflow {
184 pub trigger: Trigger,
186
187 pub component: Component,
189
190 pub submit: Submit,
192}
193
194impl Workflow {
195 pub const DEFAULT_FUEL_LIMIT: u64 = u64::MAX;
196 pub const DEFAULT_TIME_LIMIT_SECONDS: u64 = u64::MAX;
197}
198
199#[derive(Hash, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
201#[serde(rename_all = "snake_case")]
202pub enum Trigger {
203 CosmosContractEvent {
205 #[schema(value_type = String)]
206 address: layer_climb_address::Address,
207 chain: ChainKey,
208 event_type: String,
209 },
210 EvmContractEvent {
211 #[schema(value_type = String)]
212 address: alloy_primitives::Address,
213 chain: ChainKey,
214 event_hash: ByteArray<32>,
215 },
216 BlockInterval {
217 chain: ChainKey,
219 #[schema(value_type = u32)]
221 n_blocks: NonZeroU32,
222 #[schema(value_type = Option<u64>)]
224 start_block: Option<NonZeroU64>,
225 #[schema(value_type = Option<u64>)]
227 end_block: Option<NonZeroU64>,
228 },
229 Cron {
230 schedule: String,
232 start_time: Option<Timestamp>,
234 end_time: Option<Timestamp>,
236 },
237 Manual,
239}
240
241#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
243pub enum TriggerData {
244 CosmosContractEvent {
245 #[schema(value_type = String)]
247 contract_address: layer_climb_address::Address,
248 chain: ChainKey,
250 #[schema(value_type = Object)]
252 event: cosmwasm_std::Event,
253 block_height: u64,
255 event_index: u64,
257 },
258 EvmContractEvent {
259 chain: ChainKey,
261 #[schema(value_type = String)]
263 contract_address: alloy_primitives::Address,
264 #[schema(value_type = Object)]
266 log_data: LogData,
267 #[schema(value_type = String)]
269 tx_hash: alloy_primitives::TxHash,
270 block_number: u64,
272 log_index: u64,
274 #[schema(value_type = String)]
277 block_hash: alloy_primitives::B256,
278 block_timestamp: Option<u64>,
282 tx_index: u64,
284 },
285 BlockInterval {
286 chain: ChainKey,
288 block_height: u64,
290 },
291 Cron {
292 trigger_time: Timestamp,
294 },
295 Raw(Vec<u8>),
296}
297
298impl Default for TriggerData {
299 fn default() -> Self {
300 Self::new_raw(vec![])
301 }
302}
303
304impl TriggerData {
305 pub fn new_raw(data: impl AsRef<[u8]>) -> Self {
306 TriggerData::Raw(data.as_ref().to_vec())
307 }
308
309 pub fn trigger_type(&self) -> &str {
310 match self {
311 TriggerData::CosmosContractEvent { .. } => "cosmos_contract_event",
312 TriggerData::EvmContractEvent { .. } => "evm_contract_event",
313 TriggerData::BlockInterval { .. } => "block_interval",
314 TriggerData::Cron { .. } => "cron",
315 TriggerData::Raw(_) => "manual",
316 }
317 }
318
319 pub fn chain(&self) -> Option<&ChainKey> {
320 match self {
321 TriggerData::CosmosContractEvent { chain, .. }
322 | TriggerData::EvmContractEvent { chain, .. }
323 | TriggerData::BlockInterval { chain, .. } => Some(chain),
324 TriggerData::Cron { .. } | TriggerData::Raw(_) => None,
325 }
326 }
327}
328
329#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, bincode::Decode, bincode::Encode)]
331pub struct TriggerAction {
332 #[bincode(with_serde)]
333 pub config: TriggerConfig,
335
336 #[bincode(with_serde)]
337 pub data: TriggerData,
339}
340
341#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
342pub struct TriggerConfig {
344 pub service_id: ServiceId,
345 pub workflow_id: WorkflowId,
346 pub trigger: Trigger,
347}
348
349#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
351#[serde(rename_all = "snake_case")]
352pub enum Submit {
353 None,
355 Aggregator {
356 url: String,
358 component: Box<Component>,
360 signature_kind: SignatureKind,
361 },
362}
363
364#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
386pub struct SignatureKind {
387 pub algorithm: SignatureAlgorithm,
392
393 pub prefix: Option<SignaturePrefix>,
399}
400
401impl SignatureKind {
402 pub fn evm_default() -> Self {
403 Self {
404 algorithm: SignatureAlgorithm::Secp256k1,
405 prefix: Some(SignaturePrefix::Eip191),
406 }
407 }
408}
409
410#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
411#[serde(rename_all = "snake_case")]
412pub enum SignatureAlgorithm {
413 Secp256k1,
414 }
416
417#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
418#[serde(rename_all = "snake_case")]
419pub enum SignaturePrefix {
420 Eip191,
421}
422
423#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
424#[serde(rename_all = "snake_case")]
425pub enum Aggregator {
426 Evm(EvmContractSubmission),
427}
428
429#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
430#[serde(rename_all = "snake_case")]
431pub struct EvmContractSubmission {
432 pub chain: ChainKey,
433 #[schema(value_type = String)]
435 pub address: alloy_primitives::Address,
436 pub max_gas: Option<u64>,
440}
441
442impl EvmContractSubmission {
443 pub fn new(chain: ChainKey, address: alloy_primitives::Address, max_gas: Option<u64>) -> Self {
444 Self {
445 chain,
446 address,
447 max_gas,
448 }
449 }
450}
451
452#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Copy, ToSchema)]
453#[serde(rename_all = "snake_case")]
454pub enum ServiceStatus {
455 Active,
456 Paused,
459}
460
461impl FromStr for ServiceStatus {
462 type Err = anyhow::Error;
463
464 fn from_str(s: &str) -> Result<Self, Self::Err> {
465 match s.to_lowercase().as_str() {
466 "active" => Ok(ServiceStatus::Active),
467 "paused" => Ok(ServiceStatus::Paused),
468 _ => Err(anyhow::anyhow!("Invalid service status: {}", s)),
469 }
470 }
471}
472
473#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
474#[serde(default, rename_all = "snake_case")]
475#[derive(Default)]
476pub struct Permissions {
477 pub allowed_http_hosts: AllowedHostPermission,
479 pub file_system: bool,
481}
482
483#[test]
484fn permission_defaults() {
485 let permissions_json: Permissions = serde_json::from_str("{}").unwrap();
486 let permissions_default: Permissions = Permissions::default();
487
488 assert_eq!(permissions_json, permissions_default);
489 assert_eq!(
490 permissions_default.allowed_http_hosts,
491 AllowedHostPermission::None
492 );
493 assert!(!permissions_default.file_system);
494}
495
496#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, ToSchema)]
499#[serde(rename_all = "snake_case")]
500pub enum AllowedHostPermission {
501 All,
502 Only(Vec<String>),
503 #[default]
504 None,
505}
506
507#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
508#[serde(default, rename_all = "snake_case")]
509#[derive(Default)]
510pub struct WasmResponse {
511 #[serde(with = "hex")]
512 pub payload: Vec<u8>,
513 pub ordering: Option<u64>,
514}
515
516mod test_ext {
519 use std::{
520 collections::{BTreeMap, BTreeSet},
521 num::NonZeroU32,
522 };
523
524 use crate::{
525 ByteArray, ChainKey, ChainKeyError, ComponentSource, ServiceId, WorkflowId, WorkflowIdError,
526 };
527
528 use super::{Component, Trigger, TriggerConfig};
529
530 impl Component {
531 pub fn new(source: ComponentSource) -> Component {
532 Self {
533 source,
534 permissions: Default::default(),
535 fuel_limit: None,
536 time_limit_seconds: None,
537 config: BTreeMap::new(),
538 env_keys: BTreeSet::new(),
539 }
540 }
541 }
542
543 impl Trigger {
544 pub fn cosmos_contract_event(
545 address: layer_climb_address::Address,
546 chain: impl TryInto<ChainKey, Error = ChainKeyError>,
547 event_type: impl ToString,
548 ) -> Self {
549 Trigger::CosmosContractEvent {
550 address,
551 chain: chain.try_into().unwrap(),
552 event_type: event_type.to_string(),
553 }
554 }
555 pub fn evm_contract_event(
556 address: alloy_primitives::Address,
557 chain: impl TryInto<ChainKey, Error = ChainKeyError>,
558 event_hash: ByteArray<32>,
559 ) -> Self {
560 Trigger::EvmContractEvent {
561 address,
562 chain: chain.try_into().unwrap(),
563 event_hash,
564 }
565 }
566 }
567
568 impl TriggerConfig {
569 pub fn cosmos_contract_event(
570 service_id: ServiceId,
571 workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
572 contract_address: layer_climb_address::Address,
573 chain: impl TryInto<ChainKey, Error = ChainKeyError>,
574 event_type: impl ToString,
575 ) -> Self {
576 Self {
577 service_id,
578 workflow_id: workflow_id.try_into().unwrap(),
579 trigger: Trigger::cosmos_contract_event(contract_address, chain, event_type),
580 }
581 }
582
583 pub fn evm_contract_event(
584 service_id: ServiceId,
585 workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
586 contract_address: alloy_primitives::Address,
587 chain: impl TryInto<ChainKey, Error = ChainKeyError>,
588 event_hash: ByteArray<32>,
589 ) -> Self {
590 Self {
591 service_id,
592 workflow_id: workflow_id.try_into().unwrap(),
593 trigger: Trigger::evm_contract_event(contract_address, chain, event_hash),
594 }
595 }
596
597 pub fn block_interval_event(
598 service_id: ServiceId,
599 workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
600 chain: impl TryInto<ChainKey, Error = ChainKeyError>,
601 n_blocks: NonZeroU32,
602 ) -> Self {
603 Self {
604 service_id,
605 workflow_id: workflow_id.try_into().unwrap(),
606 trigger: Trigger::BlockInterval {
607 chain: chain.try_into().unwrap(),
608 n_blocks,
609 start_block: None,
610 end_block: None,
611 },
612 }
613 }
614
615 #[cfg(test)]
616 pub fn manual(
617 service_id: ServiceId,
618 workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
619 ) -> Self {
620 Self {
621 service_id,
622 workflow_id: workflow_id.try_into().unwrap(),
623 trigger: Trigger::Manual,
624 }
625 }
626 }
627}