wavs_types/
service.rs

1use std::collections::BTreeMap;
2
3use alloy_primitives::LogData;
4use serde::{Deserialize, Serialize};
5
6use crate::{digest::Digest, ByteArray};
7
8use super::{ChainName, ComponentID, ServiceID, WorkflowID};
9
10#[derive(Serialize, Deserialize, Clone, Debug)]
11#[serde(rename_all = "snake_case")]
12pub struct Service {
13    // Public identifier. Must be unique for all services
14    pub id: ServiceID,
15
16    /// This is any utf-8 string, for human-readable display.
17    pub name: String,
18
19    /// We will supoort multiple components in one service with unique service-scoped IDs. For now, just add one called "default".
20    /// This allows clean mapping from backwards-compatible API endpoints.
21    pub components: BTreeMap<ComponentID, Component>,
22
23    /// We will support multiple workflows in one service with unique service-scoped IDs. For now, only one called "default".
24    /// The workflows reference components by name (for now, always "default").
25    pub workflows: BTreeMap<WorkflowID, Workflow>,
26
27    pub status: ServiceStatus,
28
29    pub config: ServiceConfig,
30}
31
32impl Service {
33    pub fn new_simple(
34        id: ServiceID,
35        name: Option<String>,
36        trigger: Trigger,
37        component_digest: Digest,
38        submit: Submit,
39        config: Option<ServiceConfig>,
40    ) -> Self {
41        let component_id = ComponentID::default();
42        let workflow_id = WorkflowID::default();
43
44        let workflow = Workflow {
45            trigger,
46            component: component_id,
47            submit,
48        };
49
50        let component = Component {
51            wasm: component_digest,
52            permissions: Permissions::default(),
53        };
54
55        let components = BTreeMap::from([(workflow.component.clone(), component)]);
56
57        let workflows = BTreeMap::from([(workflow_id, workflow)]);
58
59        Self {
60            name: name.unwrap_or_else(|| id.to_string()),
61            id,
62            components,
63            workflows,
64            status: ServiceStatus::Active,
65            config: config.unwrap_or_default(),
66        }
67    }
68}
69
70#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
71#[serde(rename_all = "snake_case")]
72pub struct Component {
73    pub wasm: Digest,
74    // What permissions this component has.
75    // These are currently not enforced, you can pass in Default::default() for now
76    pub permissions: Permissions,
77}
78
79// FIXME: happy for a better name.
80/// This captures the triggers we listen to, the components we run, and how we submit the result
81#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
82#[serde(rename_all = "snake_case")]
83pub struct Workflow {
84    pub trigger: Trigger,
85    /// A reference to which component to run with this data - for now, always "default"
86    pub component: ComponentID,
87    /// How to submit the result of the component.
88    pub submit: Submit,
89}
90
91// The TriggerManager reacts to these triggers
92#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
93#[serde(rename_all = "snake_case")]
94pub enum Trigger {
95    // A contract that emits an event
96    CosmosContractEvent {
97        address: layer_climb_address::Address,
98        chain_name: ChainName,
99        event_type: String,
100    },
101    EthContractEvent {
102        address: alloy_primitives::Address,
103        chain_name: ChainName,
104        event_hash: ByteArray<32>,
105    },
106    // not a real trigger, just for testing
107    Manual,
108}
109
110/// The data that came from the trigger and is passed to the component after being converted into the WIT-friendly type
111#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
112pub enum TriggerData {
113    CosmosContractEvent {
114        /// The address of the contract that emitted the event
115        contract_address: layer_climb_address::Address,
116        /// The chain name of the chain where the event was emitted
117        chain_name: ChainName,
118        /// The data that was emitted by the contract
119        event: cosmwasm_std::Event,
120        /// The block height where the event was emitted
121        block_height: u64,
122    },
123    EthContractEvent {
124        /// The address of the contract that emitted the event
125        contract_address: alloy_primitives::Address,
126        /// The chain name of the chain where the event was emitted
127        chain_name: ChainName,
128        /// The raw event log
129        log: LogData,
130        /// The block height where the event was emitted
131        block_height: u64,
132    },
133    Raw(Vec<u8>),
134}
135
136impl TriggerData {
137    pub fn new_raw(data: impl AsRef<[u8]>) -> Self {
138        TriggerData::Raw(data.as_ref().to_vec())
139    }
140}
141
142/// A bundle of the trigger and the associated data needed to take action on it
143#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
144pub struct TriggerAction {
145    /// Identify which trigger this came from
146    pub config: TriggerConfig,
147
148    /// The data that came from the trigger
149    pub data: TriggerData,
150}
151
152#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
153// Trigger with metadata so it can be identified in relation to services and workflows
154pub struct TriggerConfig {
155    pub service_id: ServiceID,
156    pub workflow_id: WorkflowID,
157    pub trigger: Trigger,
158}
159
160// TODO - rename this? Trigger is a noun, Submit is a verb.. feels a bit weird
161#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
162#[serde(rename_all = "snake_case")]
163pub enum Submit {
164    // useful for when the component just does something with its own state
165    None,
166    // Ethereum Contract which implements the ILayerService interface
167    EthereumContract {
168        chain_name: ChainName,
169        address: alloy_primitives::Address,
170        max_gas: Option<u64>,
171    },
172}
173
174#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
175#[serde(rename_all = "snake_case")]
176pub struct ServiceConfig {
177    /// The maximum amount of compute metering to allow for a single execution
178    pub fuel_limit: u64,
179    /// External env variable keys to be read from the system host on execute (i.e. API keys).
180    /// Must be prefixed with `WAVS_ENV_`.
181    pub host_envs: Vec<String>,
182    /// Configuration key-value pairs that are accessible in the components environment.
183    /// These config values are public and viewable by anyone.
184    /// Components read the values with `std::env::var`, case sensitive & no prefix required.
185    /// Values here are viewable by anyone. Use host_envs to set private values.
186    pub kv: Vec<(String, String)>,
187    /// The maximum on chain gas to use for a submission
188    pub max_gas: Option<u64>,
189}
190
191impl Default for ServiceConfig {
192    fn default() -> Self {
193        Self {
194            fuel_limit: 100_000_000,
195            max_gas: None,
196            host_envs: vec![],
197            kv: vec![],
198        }
199    }
200}
201
202#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Copy)]
203#[serde(rename_all = "snake_case")]
204pub enum ServiceStatus {
205    Active,
206    // we could have more like Stopped, Failed, Cooldown, etc.
207}
208
209#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
210#[serde(default, rename_all = "snake_case")]
211#[derive(Default)]
212pub struct Permissions {
213    /// If it can talk to http hosts on the network
214    pub allowed_http_hosts: AllowedHostPermission,
215    /// If it can write to it's own local directory in the filesystem
216    pub file_system: bool,
217}
218
219// TODO: remove / change defaults?
220
221#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq)]
222#[serde(rename_all = "snake_case")]
223pub enum AllowedHostPermission {
224    All,
225    Only(Vec<String>),
226    #[default]
227    None,
228}
229
230// TODO - these shouldn't be needed in main code... gate behind `debug_assertions`
231// will need to go through use-cases of `test-utils`, maybe move into layer-tests or something
232mod test_ext {
233    use crate::{digest::Digest, id::ChainName, ByteArray, IDError, ServiceID, WorkflowID};
234
235    use super::{Component, Submit, Trigger, TriggerConfig};
236
237    impl Submit {
238        pub fn eth_contract(
239            chain_name: ChainName,
240            address: alloy_primitives::Address,
241            max_gas: Option<u64>,
242        ) -> Submit {
243            Submit::EthereumContract {
244                chain_name,
245                address,
246                max_gas,
247            }
248        }
249    }
250
251    impl Component {
252        pub fn new(digest: Digest) -> Component {
253            Self {
254                wasm: digest,
255                permissions: Default::default(),
256            }
257        }
258    }
259
260    impl Trigger {
261        pub fn cosmos_contract_event(
262            address: layer_climb_address::Address,
263            chain_name: impl Into<ChainName>,
264            event_type: impl ToString,
265        ) -> Self {
266            Trigger::CosmosContractEvent {
267                address,
268                chain_name: chain_name.into(),
269                event_type: event_type.to_string(),
270            }
271        }
272        pub fn eth_contract_event(
273            address: alloy_primitives::Address,
274            chain_name: impl Into<ChainName>,
275            event_hash: ByteArray<32>,
276        ) -> Self {
277            Trigger::EthContractEvent {
278                address,
279                chain_name: chain_name.into(),
280                event_hash,
281            }
282        }
283    }
284
285    impl TriggerConfig {
286        pub fn cosmos_contract_event(
287            service_id: impl TryInto<ServiceID, Error = IDError>,
288            workflow_id: impl TryInto<WorkflowID, Error = IDError>,
289            contract_address: layer_climb_address::Address,
290            chain_name: impl Into<ChainName>,
291            event_type: impl ToString,
292        ) -> Result<Self, IDError> {
293            Ok(Self {
294                service_id: service_id.try_into()?,
295                workflow_id: workflow_id.try_into()?,
296                trigger: Trigger::cosmos_contract_event(contract_address, chain_name, event_type),
297            })
298        }
299
300        pub fn eth_contract_event(
301            service_id: impl TryInto<ServiceID, Error = IDError>,
302            workflow_id: impl TryInto<WorkflowID, Error = IDError>,
303            contract_address: alloy_primitives::Address,
304            chain_name: impl Into<ChainName>,
305            event_hash: ByteArray<32>,
306        ) -> Result<Self, IDError> {
307            Ok(Self {
308                service_id: service_id.try_into()?,
309                workflow_id: workflow_id.try_into()?,
310                trigger: Trigger::eth_contract_event(contract_address, chain_name, event_hash),
311            })
312        }
313
314        pub fn manual(
315            service_id: impl TryInto<ServiceID, Error = IDError>,
316            workflow_id: impl TryInto<WorkflowID, Error = IDError>,
317        ) -> Result<Self, IDError> {
318            Ok(Self {
319                service_id: service_id.try_into()?,
320                workflow_id: workflow_id.try_into()?,
321                trigger: Trigger::Manual,
322            })
323        }
324    }
325}