Skip to main content

wavs_types/
service.rs

1use alloy_primitives::LogData;
2use anyhow::bail;
3use iri_string::types::UriString;
4use semver::Version;
5use serde::{Deserialize, Serialize};
6use std::collections::{BTreeMap, BTreeSet};
7use std::num::{NonZeroU32, NonZeroU64};
8use std::str::FromStr;
9use thiserror::Error;
10use utoipa::ToSchema;
11use wasm_pkg_common::package::PackageRef;
12
13#[cfg(feature = "ts-bindings")]
14use ts_rs::TS;
15
16use crate::{ByteArray, ComponentDigest, ServiceDigest, Timestamp};
17
18use super::{ChainKey, ServiceId, WorkflowId};
19
20/// ATProto Jetstream commit action types
21#[cfg_attr(feature = "ts-bindings", derive(TS))]
22#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, ToSchema, Hash)]
23#[serde(rename_all = "snake_case")]
24pub enum AtProtoAction {
25    /// Create a new record
26    Create,
27    /// Update an existing record
28    Update,
29    /// Delete a record
30    Delete,
31}
32
33impl std::fmt::Display for AtProtoAction {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            AtProtoAction::Create => write!(f, "create"),
37            AtProtoAction::Update => write!(f, "update"),
38            AtProtoAction::Delete => write!(f, "delete"),
39        }
40    }
41}
42
43impl FromStr for AtProtoAction {
44    type Err = anyhow::Error;
45
46    fn from_str(s: &str) -> Result<Self, Self::Err> {
47        match s.to_lowercase().as_str() {
48            "create" => Ok(AtProtoAction::Create),
49            "update" => Ok(AtProtoAction::Update),
50            "delete" => Ok(AtProtoAction::Delete),
51            _ => bail!(
52                "Invalid action '{}'. Must be one of: create, update, delete",
53                s
54            ),
55        }
56    }
57}
58
59#[derive(Error, Debug)]
60pub enum ServiceError {
61    #[error("Failed to serialize service for hashing: {0}")]
62    SerializationError(#[from] serde_json::Error),
63}
64
65/// Service validation is a runtime check, and depends on:
66///
67/// 1. All service handlers on a given chain use the same service manager
68/// 2. All service managers on non-source chains properly mirror the operator set of the source
69/// 3. All components are legitimate (e.g. can be downloaded, match the provided digest, execute as expected, etc.)
70#[cfg_attr(feature = "ts-bindings", derive(TS))]
71#[cfg_attr(feature = "ts-bindings", ts(export))]
72#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
73#[serde(rename_all = "snake_case")]
74pub struct Service {
75    /// This is any utf-8 string, for human-readable display.
76    pub name: String,
77
78    /// We support multiple workflows in one service with unique service-scoped IDs.
79    pub workflows: BTreeMap<WorkflowId, Workflow>,
80
81    pub status: ServiceStatus,
82
83    pub manager: ServiceManager,
84}
85
86impl Service {
87    // this is only used for local/tests, but we want to keep it consistent
88    pub fn hash(&self) -> Result<ServiceDigest, ServiceError> {
89        let service_bytes = serde_json::to_vec(self)?;
90        Ok(ServiceDigest::hash(&service_bytes))
91    }
92
93    pub fn id(&self) -> ServiceId {
94        ServiceId::from(&self.manager)
95    }
96}
97
98#[cfg_attr(feature = "ts-bindings", derive(TS))]
99#[cfg_attr(feature = "ts-bindings", ts(export))]
100#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema, PartialOrd, Ord)]
101#[serde(rename_all = "snake_case")]
102pub enum ServiceManager {
103    Evm {
104        chain: ChainKey,
105        #[schema(value_type = String)]
106        #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
107        address: alloy_primitives::Address,
108    },
109    Cosmos {
110        chain: ChainKey,
111        #[schema(value_type = String)]
112        #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
113        address: layer_climb_address::CosmosAddr,
114    },
115}
116
117impl From<&ServiceManager> for ServiceId {
118    fn from(manager: &ServiceManager) -> Self {
119        match manager {
120            ServiceManager::Evm { chain, address } => {
121                let mut bytes = Vec::new();
122                bytes.extend_from_slice(b"evm");
123                bytes.extend_from_slice(chain.to_string().as_bytes());
124                bytes.extend_from_slice(address.as_slice());
125                ServiceId::hash(bytes)
126            }
127            ServiceManager::Cosmos { chain, address } => {
128                let mut bytes = Vec::new();
129                bytes.extend_from_slice(b"cosmos");
130                bytes.extend_from_slice(chain.to_string().as_bytes());
131                bytes.extend_from_slice(&address.to_vec());
132                ServiceId::hash(bytes)
133            }
134        }
135    }
136}
137
138impl ServiceManager {
139    pub fn chain(&self) -> &ChainKey {
140        match self {
141            ServiceManager::Evm { chain, .. } => chain,
142            ServiceManager::Cosmos { chain, .. } => chain,
143        }
144    }
145    pub fn address(&self) -> layer_climb_address::Address {
146        match self {
147            ServiceManager::Evm { address, .. } => (*address).into(),
148            ServiceManager::Cosmos { address, .. } => address.clone().into(),
149        }
150    }
151}
152
153impl Service {
154    pub fn new_simple(
155        name: Option<String>,
156        trigger: Trigger,
157        source: ComponentSource,
158        submit: Submit,
159        manager: ServiceManager,
160    ) -> Self {
161        let workflow_id = WorkflowId::default();
162
163        let workflow = Workflow {
164            trigger,
165            component: Component::new(source),
166            submit,
167        };
168
169        let workflows = BTreeMap::from([(workflow_id, workflow)]);
170
171        Self {
172            name: name.unwrap_or_else(|| "Unknown".to_string()),
173            workflows,
174            status: ServiceStatus::Active,
175            manager,
176        }
177    }
178}
179
180#[cfg_attr(feature = "ts-bindings", derive(TS))]
181#[cfg_attr(feature = "ts-bindings", ts(export))]
182#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
183#[serde(rename_all = "snake_case")]
184pub struct Component {
185    pub source: ComponentSource,
186
187    // What permissions this component has.
188    // These are currently not enforced, you can pass in Default::default() for now
189    pub permissions: Permissions,
190
191    /// The maximum amount of compute metering to allow for a single component execution
192    /// If not supplied, will be `Workflow::DEFAULT_FUEL_LIMIT`
193    pub fuel_limit: Option<u64>,
194
195    /// The maximum amount of time to allow for a single component execution, in seconds
196    /// If not supplied, default will be `Workflow::DEFAULT_TIME_LIMIT_SECONDS`
197    pub time_limit_seconds: Option<u64>,
198
199    /// Key-value pairs that are accessible in the components via host bindings.
200    pub config: BTreeMap<String, String>,
201
202    /// External env variable keys to be read from the system host on execute (i.e. API keys).
203    /// Must be prefixed with `WAVS_ENV_`.
204    pub env_keys: BTreeSet<String>,
205}
206
207#[cfg_attr(feature = "ts-bindings", derive(TS))]
208#[cfg_attr(feature = "ts-bindings", ts(export))]
209#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ToSchema)]
210#[serde(rename_all = "snake_case")]
211pub enum ComponentSource {
212    /// The wasm bytecode provided at fixed url, digest provided to ensure no tampering
213    Download {
214        #[schema(value_type = String)]
215        #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
216        uri: UriString,
217        #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
218        digest: ComponentDigest,
219    },
220    /// The wasm bytecode downloaded from a standard registry, digest provided to ensure no tampering
221    Registry {
222        #[serde(flatten)]
223        registry: Registry,
224    },
225    /// An already deployed component
226    #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
227    Digest(ComponentDigest),
228}
229
230#[cfg_attr(feature = "ts-bindings", derive(TS))]
231#[cfg_attr(feature = "ts-bindings", ts(export))]
232#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ToSchema)]
233pub struct Registry {
234    pub digest: ComponentDigest,
235    /// Optional domain to use for a registry (such as ghcr.io)
236    /// if default of wa.dev (or whatever wavs uses in the future)
237    /// is not desired by user
238    pub domain: Option<String>,
239    /// Optional semver value, if absent then latest is used
240    #[schema(value_type = Option<String>)]
241    #[cfg_attr(feature = "ts-bindings", ts(type = "string | null"))]
242    pub version: Option<Version>,
243    /// Package identifier of form <namespace>:<packagename>
244    #[schema(value_type = String)]
245    #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
246    pub package: PackageRef,
247}
248
249impl ComponentSource {
250    pub fn digest(&self) -> &ComponentDigest {
251        match self {
252            ComponentSource::Download { digest, .. } => digest,
253            ComponentSource::Registry { registry } => &registry.digest,
254            ComponentSource::Digest(digest) => digest,
255        }
256    }
257}
258
259// FIXME: happy for a better name.
260/// This captures the triggers we listen to, the components we run, and how we submit the result
261#[cfg_attr(feature = "ts-bindings", derive(TS))]
262#[cfg_attr(feature = "ts-bindings", ts(export))]
263#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
264#[serde(rename_all = "snake_case")]
265pub struct Workflow {
266    /// The trigger that fires this workflow
267    pub trigger: Trigger,
268
269    /// The component to run when the trigger fires
270    pub component: Component,
271
272    /// How to submit the result of the component.
273    pub submit: Submit,
274}
275
276impl Workflow {
277    pub const DEFAULT_FUEL_LIMIT: u64 = u64::MAX;
278    pub const DEFAULT_TIME_LIMIT_SECONDS: u64 = u64::MAX;
279}
280
281// The TriggerManager reacts to these triggers
282#[cfg_attr(feature = "ts-bindings", derive(TS))]
283#[cfg_attr(feature = "ts-bindings", ts(export))]
284#[derive(Hash, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
285#[serde(rename_all = "snake_case")]
286pub enum Trigger {
287    // A contract that emits an event
288    CosmosContractEvent {
289        #[schema(value_type = String)]
290        #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
291        address: layer_climb_address::CosmosAddr,
292        chain: ChainKey,
293        event_type: String,
294    },
295    EvmContractEvent {
296        #[schema(value_type = String)]
297        #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
298        address: alloy_primitives::Address,
299        chain: ChainKey,
300        #[cfg_attr(feature = "ts-bindings", ts(type = "string"))]
301        event_hash: ByteArray<32>,
302    },
303    BlockInterval {
304        /// The chain to use for the block interval
305        chain: ChainKey,
306        /// Number of blocks to wait between each execution
307        #[schema(value_type = u32)]
308        n_blocks: NonZeroU32,
309        /// Optional start block height indicating when the interval begins.
310        #[schema(value_type = Option<u64>)]
311        start_block: Option<NonZeroU64>,
312        /// Optional end block height indicating when the interval begins.
313        #[schema(value_type = Option<u64>)]
314        end_block: Option<NonZeroU64>,
315    },
316    Cron {
317        /// A cron expression defining the schedule for execution.
318        schedule: String,
319        /// Optional start time (timestamp in nanoseconds) indicating when the schedule begins.
320        start_time: Option<Timestamp>,
321        /// Optional end time (timestamp in nanoseconds) indicating when the schedule ends.
322        end_time: Option<Timestamp>,
323    },
324    /// ATProto Jetstream event trigger
325    AtProtoEvent {
326        /// Collection NSID to filter for (e.g., "app.bsky.feed.post")
327        /// Supports wildcards with prefix matching (e.g., "app.bsky.feed.*")
328        collection: String,
329        /// Optional DID to filter for specific repositories
330        /// If None, will match events from any repository
331        repo_did: Option<String>,
332        /// Action type to filter for (create, update, delete)
333        /// If None, will match all action types
334        action: Option<AtProtoAction>,
335    },
336    /// Hypercore append event trigger
337    HypercoreAppend {
338        /// Feed key to filter on.
339        feed_key: String,
340    },
341    // not a real trigger, just for testing
342    Manual,
343}
344
345/// The data that came from the trigger and is passed to the component after being converted into the WIT-friendly type
346#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
347pub enum TriggerData {
348    CosmosContractEvent {
349        /// The address of the contract that emitted the event
350        #[schema(value_type = String)]
351        contract_address: layer_climb_address::CosmosAddr,
352        /// The chain where the event was emitted
353        chain: ChainKey,
354        /// The data that was emitted by the contract
355        #[schema(value_type = Object)]
356        event: cosmwasm_std::Event,
357        /// The block height where the event was emitted
358        block_height: u64,
359        /// The index of the event in this block, required for unique identification
360        event_index: u64,
361    },
362    EvmContractEvent {
363        /// The chain where the event was emitted
364        chain: ChainKey,
365        /// The address of the contract that emitted the event
366        #[schema(value_type = String)]
367        contract_address: alloy_primitives::Address,
368        /// The log data
369        #[schema(value_type = Object)]
370        log_data: LogData,
371        /// The transaction hash where the event was emitted
372        #[schema(value_type = String)]
373        tx_hash: alloy_primitives::TxHash,
374        /// The block height where the event was emitted
375        block_number: u64,
376        /// The index of the log in the block
377        log_index: u64,
378        // these are all optional because they may not be present in the log and we don't need them
379        /// Hash of the block the transaction that emitted this log was mined in
380        #[schema(value_type = String)]
381        block_hash: alloy_primitives::B256,
382        /// The timestamp of the block containing this event, as proposed in https://github.com/ethereum/execution-apis/issues/295
383        /// This field is optional since nodes are not required to include it in event logs.
384        /// If not provided, applications may need to fetch the block header directly to obtain the timestamp.
385        block_timestamp: Option<u64>,
386        /// Index of the Transaction in the block
387        tx_index: u64,
388    },
389    BlockInterval {
390        /// The chain where the blocks are checked
391        chain: ChainKey,
392        /// The block height where the event was emitted
393        block_height: u64,
394    },
395    Cron {
396        /// The trigger time
397        trigger_time: Timestamp,
398    },
399    /// ATProto Jetstream event data
400    AtProtoEvent {
401        /// Sequence number of the event in the stream
402        sequence: i64,
403        /// Timestamp in microseconds
404        timestamp: i64,
405        /// Repository DID that generated the event
406        repo: String,
407        /// Collection NSID (e.g., "app.bsky.feed.post")
408        collection: String,
409        /// Record key within the collection
410        rkey: String,
411        /// Action type (create, update, delete)
412        action: AtProtoAction,
413        /// CID of the record (None for delete events)
414        cid: Option<String>,
415        /// Record data as JSON (None for delete events)
416        record: Option<serde_json::Value>,
417        /// Repository revision identifier for this commit (if provided by the event)
418        rev: Option<String>,
419        /// Index of the operation within the commit (0-based)
420        op_index: Option<u32>,
421    },
422    HypercoreAppend {
423        /// Hypercore feed key that emitted the append
424        feed_key: String,
425        /// Index of the appended entry
426        index: u64,
427        /// Raw entry data
428        data: Vec<u8>,
429    },
430    Raw(Vec<u8>),
431}
432
433impl Default for TriggerData {
434    fn default() -> Self {
435        Self::new_raw(vec![])
436    }
437}
438
439impl TriggerData {
440    pub fn new_raw(data: impl AsRef<[u8]>) -> Self {
441        TriggerData::Raw(data.as_ref().to_vec())
442    }
443
444    pub fn trigger_type(&self) -> &str {
445        match self {
446            TriggerData::CosmosContractEvent { .. } => "cosmos_contract_event",
447            TriggerData::EvmContractEvent { .. } => "evm_contract_event",
448            TriggerData::BlockInterval { .. } => "block_interval",
449            TriggerData::Cron { .. } => "cron",
450            TriggerData::AtProtoEvent { .. } => "atproto_event",
451            TriggerData::HypercoreAppend { .. } => "hypercore_append",
452            TriggerData::Raw(_) => "manual",
453        }
454    }
455
456    pub fn chain(&self) -> Option<&ChainKey> {
457        match self {
458            TriggerData::CosmosContractEvent { chain, .. }
459            | TriggerData::EvmContractEvent { chain, .. }
460            | TriggerData::BlockInterval { chain, .. } => Some(chain),
461            TriggerData::Cron { .. }
462            | TriggerData::AtProtoEvent { .. }
463            | TriggerData::HypercoreAppend { .. }
464            | TriggerData::Raw(_) => None,
465        }
466    }
467}
468
469/// A bundle of the trigger and the associated data needed to take action on it
470#[derive(
471    Serialize, Deserialize, Clone, Debug, PartialEq, Eq, bincode::Decode, bincode::Encode, ToSchema,
472)]
473pub struct TriggerAction {
474    #[bincode(with_serde)]
475    /// Identify which trigger this came from
476    pub config: TriggerConfig,
477
478    #[bincode(with_serde)]
479    /// The data that came from the trigger
480    pub data: TriggerData,
481}
482
483#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
484// Trigger with metadata so it can be identified in relation to services and workflows
485pub struct TriggerConfig {
486    pub service_id: ServiceId,
487    pub workflow_id: WorkflowId,
488    pub trigger: Trigger,
489}
490
491// TODO - rename this? Trigger is a noun, Submit is a verb.. feels a bit weird
492#[cfg_attr(feature = "ts-bindings", derive(TS))]
493#[cfg_attr(feature = "ts-bindings", ts(export))]
494#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
495#[serde(rename_all = "snake_case")]
496pub enum Submit {
497    // useful for when the component just does something with its own state
498    None,
499    Aggregator {
500        /// component dynamically determines the destination
501        component: Box<Component>,
502        signature_kind: SignatureKind,
503    },
504}
505
506/// Defines the signature configuration for cryptographic operations in WAVS.
507///
508/// This struct separates the cryptographic algorithm from the message formatting
509/// to provide flexibility in signature schemes while maintaining compatibility
510/// across different blockchain ecosystems.
511///
512/// ## Why Separate Algorithm and Prefix?
513///
514/// The separation of `algorithm` and `prefix` serves several important purposes:
515///
516/// 1. **Algorithm Independence**: The same cryptographic algorithm (e.g., secp256k1)
517///    can be used with different message formatting schemes. This allows the same
518///    private key to work across different contexts.
519///
520/// 2. **Ethereum Compatibility**: Some signatures need EIP-191 prefixing for
521///    Ethereum compatibility, while others work with raw message hashes. The
522///    optional prefix allows both modes.
523///
524/// 3. **Future Extensibility**: As new signature algorithms (BLS12-381, Ed25519, etc.)
525///    and prefix schemes are added, this structure can accommodate them without
526///    breaking changes.
527#[cfg_attr(feature = "ts-bindings", derive(TS))]
528#[cfg_attr(feature = "ts-bindings", ts(export))]
529#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema, Hash)]
530pub struct SignatureKind {
531    /// The cryptographic algorithm used for signature generation and verification.
532    ///
533    /// This determines the elliptic curve and mathematical operations used,
534    /// but not how the message is formatted before signing.
535    pub algorithm: SignatureAlgorithm,
536
537    /// Optional message prefix scheme applied before signing.
538    ///
539    /// When `Some(prefix)`, the message is formatted according to the specified
540    /// scheme (e.g., EIP-191 for Ethereum compatibility). When `None`, the raw
541    /// message hash is signed directly.
542    pub prefix: Option<SignaturePrefix>,
543}
544
545impl SignatureKind {
546    pub fn evm_default() -> Self {
547        Self {
548            algorithm: SignatureAlgorithm::Secp256k1,
549            prefix: Some(SignaturePrefix::Eip191),
550        }
551    }
552}
553
554#[cfg_attr(feature = "ts-bindings", derive(TS))]
555#[cfg_attr(feature = "ts-bindings", ts(export))]
556#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema, Hash)]
557#[serde(rename_all = "snake_case")]
558pub enum SignatureAlgorithm {
559    Secp256k1,
560    // Future: Bls12381, Ed25519, Secp256r1, etc.
561}
562
563#[cfg_attr(feature = "ts-bindings", derive(TS))]
564#[cfg_attr(feature = "ts-bindings", ts(export))]
565#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema, Hash)]
566#[serde(rename_all = "snake_case")]
567pub enum SignaturePrefix {
568    Eip191,
569}
570
571#[cfg_attr(feature = "ts-bindings", derive(TS))]
572#[cfg_attr(feature = "ts-bindings", ts(export))]
573#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Copy, ToSchema)]
574#[serde(rename_all = "snake_case")]
575pub enum ServiceStatus {
576    Active,
577    // Service is paused, no workflows will be executed
578    // however the service can still be queried for AVS Key etc.
579    Paused,
580}
581
582impl FromStr for ServiceStatus {
583    type Err = anyhow::Error;
584
585    fn from_str(s: &str) -> Result<Self, Self::Err> {
586        match s.to_lowercase().as_str() {
587            "active" => Ok(ServiceStatus::Active),
588            "paused" => Ok(ServiceStatus::Paused),
589            _ => Err(anyhow::anyhow!("Invalid service status: {}", s)),
590        }
591    }
592}
593
594#[cfg_attr(feature = "ts-bindings", derive(TS))]
595#[cfg_attr(feature = "ts-bindings", ts(export))]
596#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema, Default)]
597#[serde(default, rename_all = "snake_case")]
598pub struct Permissions {
599    /// If it can talk to http hosts on the network
600    pub allowed_http_hosts: AllowedHostPermission,
601    /// If it can write to it's own local directory in the filesystem
602    pub file_system: bool,
603    /// If it can use the host's raw sockets (not needed for http)
604    pub raw_sockets: bool,
605    /// If it can perform DNS resolution (not needed for http)
606    pub dns_resolution: bool,
607}
608
609#[test]
610fn permission_defaults() {
611    let permissions_json: Permissions = serde_json::from_str("{}").unwrap();
612    let permissions_default: Permissions = Permissions::default();
613
614    assert_eq!(permissions_json, permissions_default);
615    assert_eq!(
616        permissions_default.allowed_http_hosts,
617        AllowedHostPermission::None
618    );
619    assert!(!permissions_default.file_system);
620}
621
622// TODO: remove / change defaults?
623
624#[cfg_attr(feature = "ts-bindings", derive(TS))]
625#[cfg_attr(feature = "ts-bindings", ts(export))]
626#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, ToSchema)]
627#[serde(rename_all = "snake_case")]
628pub enum AllowedHostPermission {
629    All,
630    Only(Vec<String>),
631    #[default]
632    None,
633}
634
635#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
636#[serde(default, rename_all = "snake_case")]
637#[derive(Default)]
638pub struct WasmResponse {
639    #[serde(with = "const_hex")]
640    pub payload: Vec<u8>,
641    pub ordering: Option<u64>,
642    #[serde(with = "crate::serde_helpers::option_const_hex")]
643    pub event_id_salt: Option<Vec<u8>>,
644}
645
646impl WasmResponse {
647    /// Default maximum payload size: 50 MB
648    pub const DEFAULT_MAX_PAYLOAD_SIZE: usize = 50 * 1024 * 1024;
649    /// Default maximum event_id_salt size: 1 MB
650    pub const DEFAULT_MAX_SALT_SIZE: usize = 1024 * 1024;
651
652    /// Validates that the payload and salt are within size limits.
653    pub fn validate_size(
654        &self,
655        max_payload_size: usize,
656        max_salt_size: usize,
657    ) -> Result<(), WasmResponseSizeError> {
658        if self.payload.len() > max_payload_size {
659            return Err(WasmResponseSizeError::PayloadTooLarge {
660                size: self.payload.len(),
661                max: max_payload_size,
662            });
663        }
664        if let Some(salt) = &self.event_id_salt {
665            if salt.len() > max_salt_size {
666                return Err(WasmResponseSizeError::SaltTooLarge {
667                    size: salt.len(),
668                    max: max_salt_size,
669                });
670            }
671        }
672        Ok(())
673    }
674}
675
676#[derive(Debug, thiserror::Error)]
677pub enum WasmResponseSizeError {
678    #[error("Payload size {size} bytes exceeds maximum of {max} bytes")]
679    PayloadTooLarge { size: usize, max: usize },
680    #[error("Event ID salt size {size} bytes exceeds maximum of {max} bytes")]
681    SaltTooLarge { size: usize, max: usize },
682}
683
684// TODO - these shouldn't be needed in main code... gate behind `debug_assertions`
685// will need to go through use-cases of `test-utils`, maybe move into layer-tests or something
686mod test_ext {
687    use std::{
688        collections::{BTreeMap, BTreeSet},
689        num::NonZeroU32,
690    };
691
692    use crate::{
693        ByteArray, ChainKey, ChainKeyError, ComponentSource, ServiceId, WorkflowId, WorkflowIdError,
694    };
695
696    use super::{Component, Trigger, TriggerConfig};
697
698    impl Component {
699        pub fn new(source: ComponentSource) -> Component {
700            Self {
701                source,
702                permissions: Default::default(),
703                fuel_limit: None,
704                time_limit_seconds: None,
705                config: BTreeMap::new(),
706                env_keys: BTreeSet::new(),
707            }
708        }
709    }
710
711    impl Trigger {
712        pub fn cosmos_contract_event(
713            address: layer_climb_address::CosmosAddr,
714            chain: impl TryInto<ChainKey, Error = ChainKeyError>,
715            event_type: impl ToString,
716        ) -> Self {
717            Trigger::CosmosContractEvent {
718                address,
719                chain: chain.try_into().unwrap(),
720                event_type: event_type.to_string(),
721            }
722        }
723        pub fn evm_contract_event(
724            address: alloy_primitives::Address,
725            chain: impl TryInto<ChainKey, Error = ChainKeyError>,
726            event_hash: ByteArray<32>,
727        ) -> Self {
728            Trigger::EvmContractEvent {
729                address,
730                chain: chain.try_into().unwrap(),
731                event_hash,
732            }
733        }
734    }
735
736    impl TriggerConfig {
737        pub fn cosmos_contract_event(
738            service_id: ServiceId,
739            workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
740            contract_address: layer_climb_address::CosmosAddr,
741            chain: impl TryInto<ChainKey, Error = ChainKeyError>,
742            event_type: impl ToString,
743        ) -> Self {
744            Self {
745                service_id,
746                workflow_id: workflow_id.try_into().unwrap(),
747                trigger: Trigger::cosmos_contract_event(contract_address, chain, event_type),
748            }
749        }
750
751        pub fn evm_contract_event(
752            service_id: ServiceId,
753            workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
754            contract_address: alloy_primitives::Address,
755            chain: impl TryInto<ChainKey, Error = ChainKeyError>,
756            event_hash: ByteArray<32>,
757        ) -> Self {
758            Self {
759                service_id,
760                workflow_id: workflow_id.try_into().unwrap(),
761                trigger: Trigger::evm_contract_event(contract_address, chain, event_hash),
762            }
763        }
764
765        pub fn block_interval_event(
766            service_id: ServiceId,
767            workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
768            chain: impl TryInto<ChainKey, Error = ChainKeyError>,
769            n_blocks: NonZeroU32,
770        ) -> Self {
771            Self {
772                service_id,
773                workflow_id: workflow_id.try_into().unwrap(),
774                trigger: Trigger::BlockInterval {
775                    chain: chain.try_into().unwrap(),
776                    n_blocks,
777                    start_block: None,
778                    end_block: None,
779                },
780            }
781        }
782
783        #[cfg(test)]
784        pub fn manual(
785            service_id: ServiceId,
786            workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
787        ) -> Self {
788            Self {
789                service_id,
790                workflow_id: workflow_id.try_into().unwrap(),
791                trigger: Trigger::Manual,
792            }
793        }
794    }
795}