use alloy_primitives::LogData;
use semver::Version;
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
use std::num::{NonZeroU32, NonZeroU64};
use std::str::FromStr;
use utoipa::ToSchema;
use wasm_pkg_common::package::PackageRef;
use crate::{ByteArray, ComponentDigest, ServiceDigest, Timestamp};
use super::{ChainName, ServiceID, WorkflowID};
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub struct Service {
pub name: String,
pub workflows: BTreeMap<WorkflowID, Workflow>,
pub status: ServiceStatus,
pub manager: ServiceManager,
}
impl Service {
pub fn hash(&self) -> anyhow::Result<ServiceDigest> {
let service_bytes = serde_json::to_vec(self)?;
Ok(ServiceDigest::hash(&service_bytes))
}
pub fn id(&self) -> ServiceID {
ServiceID::from(&self.manager)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ServiceManager {
Evm {
chain_name: ChainName,
#[schema(value_type = String)]
address: alloy_primitives::Address,
},
}
impl From<&ServiceManager> for ServiceID {
fn from(manager: &ServiceManager) -> Self {
match manager {
ServiceManager::Evm {
chain_name,
address,
} => {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"evm");
bytes.extend_from_slice(chain_name.as_bytes());
bytes.extend_from_slice(address.as_slice());
ServiceID::hash(bytes)
}
}
}
}
impl ServiceManager {
pub fn chain_name(&self) -> &ChainName {
match self {
ServiceManager::Evm { chain_name, .. } => chain_name,
}
}
pub fn evm_address_unchecked(&self) -> alloy_primitives::Address {
match self {
ServiceManager::Evm { address, .. } => *address,
}
}
}
impl Service {
pub fn new_simple(
name: Option<String>,
trigger: Trigger,
source: ComponentSource,
submit: Submit,
manager: ServiceManager,
) -> Self {
let workflow_id = WorkflowID::default();
let workflow = Workflow {
trigger,
component: Component::new(source),
submit,
};
let workflows = BTreeMap::from([(workflow_id, workflow)]);
Self {
name: name.unwrap_or_else(|| "Unknown".to_string()),
workflows,
status: ServiceStatus::Active,
manager,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub struct Component {
pub source: ComponentSource,
pub permissions: Permissions,
pub fuel_limit: Option<u64>,
pub time_limit_seconds: Option<u64>,
pub config: BTreeMap<String, String>,
pub env_keys: BTreeSet<String>,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ToSchema)]
pub enum ComponentSource {
Download {
url: String,
digest: ComponentDigest,
},
Registry { registry: Registry },
Digest(ComponentDigest),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, ToSchema)]
pub struct Registry {
pub digest: ComponentDigest,
pub domain: Option<String>,
#[schema(value_type = Option<String>)]
pub version: Option<Version>,
#[schema(value_type = String)]
pub package: PackageRef,
}
impl ComponentSource {
pub fn digest(&self) -> &ComponentDigest {
match self {
ComponentSource::Download { digest, .. } => digest,
ComponentSource::Registry { registry } => ®istry.digest,
ComponentSource::Digest(digest) => digest,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub struct Workflow {
pub trigger: Trigger,
pub component: Component,
pub submit: Submit,
}
impl Workflow {
pub const DEFAULT_FUEL_LIMIT: u64 = 100_000_000;
pub const DEFAULT_TIME_LIMIT_SECONDS: u64 = 30;
}
#[derive(Hash, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Trigger {
CosmosContractEvent {
#[schema(value_type = Object)] address: layer_climb_address::Address,
chain_name: ChainName,
event_type: String,
},
EvmContractEvent {
#[schema(value_type = String)]
address: alloy_primitives::Address,
chain_name: ChainName,
event_hash: ByteArray<32>,
},
BlockInterval {
chain_name: ChainName,
#[schema(value_type = u32)]
n_blocks: NonZeroU32,
#[schema(value_type = Option<u64>)]
start_block: Option<NonZeroU64>,
#[schema(value_type = Option<u64>)]
end_block: Option<NonZeroU64>,
},
Cron {
schedule: String,
start_time: Option<Timestamp>,
end_time: Option<Timestamp>,
},
Manual,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub enum TriggerData {
CosmosContractEvent {
contract_address: layer_climb_address::Address,
chain_name: ChainName,
event: cosmwasm_std::Event,
block_height: u64,
},
EvmContractEvent {
contract_address: alloy_primitives::Address,
chain_name: ChainName,
log: LogData,
block_height: u64,
},
BlockInterval {
chain_name: ChainName,
block_height: u64,
},
Cron {
trigger_time: Timestamp,
},
Raw(Vec<u8>),
}
impl TriggerData {
pub fn new_raw(data: impl AsRef<[u8]>) -> Self {
TriggerData::Raw(data.as_ref().to_vec())
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, bincode::Decode, bincode::Encode)]
pub struct TriggerAction {
#[bincode(with_serde)]
pub config: TriggerConfig,
#[bincode(with_serde)]
pub data: TriggerData,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct TriggerConfig {
pub service_id: ServiceID,
pub workflow_id: WorkflowID,
pub trigger: Trigger,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Submit {
None,
Aggregator {
url: String,
component: Option<Box<Component>>,
evm_contracts: Option<Vec<EvmContractSubmission>>,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Aggregator {
Evm(EvmContractSubmission),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub struct EvmContractSubmission {
pub chain_name: ChainName,
#[schema(value_type = String)]
pub address: alloy_primitives::Address,
pub max_gas: Option<u64>,
}
impl EvmContractSubmission {
pub fn new(
chain_name: ChainName,
address: alloy_primitives::Address,
max_gas: Option<u64>,
) -> Self {
Self {
chain_name,
address,
max_gas,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Copy, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum ServiceStatus {
Active,
Paused,
}
impl FromStr for ServiceStatus {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"active" => Ok(ServiceStatus::Active),
"paused" => Ok(ServiceStatus::Paused),
_ => Err(anyhow::anyhow!("Invalid service status: {}", s)),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(default, rename_all = "snake_case")]
#[derive(Default)]
pub struct Permissions {
pub allowed_http_hosts: AllowedHostPermission,
pub file_system: bool,
}
#[test]
fn permission_defaults() {
let permissions_json: Permissions = serde_json::from_str("{}").unwrap();
let permissions_default: Permissions = Permissions::default();
assert_eq!(permissions_json, permissions_default);
assert_eq!(
permissions_default.allowed_http_hosts,
AllowedHostPermission::None
);
assert!(!permissions_default.file_system);
}
#[derive(Serialize, Deserialize, Clone, Default, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum AllowedHostPermission {
All,
Only(Vec<String>),
#[default]
None,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
#[serde(default, rename_all = "snake_case")]
#[derive(Default)]
pub struct WasmResponse {
pub payload: Vec<u8>,
pub ordering: Option<u64>,
}
mod test_ext {
use std::{
collections::{BTreeMap, BTreeSet},
num::NonZeroU32,
};
use crate::{id::ChainName, ByteArray, ComponentSource, IDError, ServiceID, WorkflowID};
use super::{Component, Trigger, TriggerConfig};
impl Component {
pub fn new(source: ComponentSource) -> Component {
Self {
source,
permissions: Default::default(),
fuel_limit: None,
time_limit_seconds: None,
config: BTreeMap::new(),
env_keys: BTreeSet::new(),
}
}
}
impl Trigger {
pub fn cosmos_contract_event(
address: layer_climb_address::Address,
chain_name: impl Into<ChainName>,
event_type: impl ToString,
) -> Self {
Trigger::CosmosContractEvent {
address,
chain_name: chain_name.into(),
event_type: event_type.to_string(),
}
}
pub fn evm_contract_event(
address: alloy_primitives::Address,
chain_name: impl Into<ChainName>,
event_hash: ByteArray<32>,
) -> Self {
Trigger::EvmContractEvent {
address,
chain_name: chain_name.into(),
event_hash,
}
}
}
impl TriggerConfig {
pub fn cosmos_contract_event(
service_id: ServiceID,
workflow_id: impl TryInto<WorkflowID, Error = IDError>,
contract_address: layer_climb_address::Address,
chain_name: impl Into<ChainName>,
event_type: impl ToString,
) -> Result<Self, IDError> {
Ok(Self {
service_id,
workflow_id: workflow_id.try_into()?,
trigger: Trigger::cosmos_contract_event(contract_address, chain_name, event_type),
})
}
pub fn evm_contract_event(
service_id: ServiceID,
workflow_id: impl TryInto<WorkflowID, Error = IDError>,
contract_address: alloy_primitives::Address,
chain_name: impl Into<ChainName>,
event_hash: ByteArray<32>,
) -> Result<Self, IDError> {
Ok(Self {
service_id,
workflow_id: workflow_id.try_into()?,
trigger: Trigger::evm_contract_event(contract_address, chain_name, event_hash),
})
}
pub fn block_interval_event(
service_id: ServiceID,
workflow_id: impl TryInto<WorkflowID, Error = IDError>,
chain_name: impl Into<ChainName>,
n_blocks: NonZeroU32,
) -> Result<Self, IDError> {
Ok(Self {
service_id,
workflow_id: workflow_id.try_into()?,
trigger: Trigger::BlockInterval {
chain_name: chain_name.into(),
n_blocks,
start_block: None,
end_block: None,
},
})
}
#[cfg(test)]
pub fn manual(
service_id: ServiceID,
workflow_id: impl TryInto<WorkflowID, Error = IDError>,
) -> Result<Self, IDError> {
Ok(Self {
service_id,
workflow_id: workflow_id.try_into()?,
trigger: Trigger::Manual,
})
}
}
}