use alloy_primitives::{hex, LogData};
use iri_string::types::UriString;
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::{ChainKey, 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, PartialOrd, Ord)]
#[serde(rename_all = "snake_case")]
pub enum ServiceManager {
Evm {
chain: ChainKey,
#[schema(value_type = String)]
address: alloy_primitives::Address,
},
Cosmos {
chain: ChainKey,
#[schema(value_type = String)]
address: layer_climb_address::CosmosAddr,
},
}
impl From<&ServiceManager> for ServiceId {
fn from(manager: &ServiceManager) -> Self {
match manager {
ServiceManager::Evm { chain, address } => {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"evm");
bytes.extend_from_slice(chain.to_string().as_bytes());
bytes.extend_from_slice(address.as_slice());
ServiceId::hash(bytes)
}
ServiceManager::Cosmos { chain, address } => {
let mut bytes = Vec::new();
bytes.extend_from_slice(b"cosmos");
bytes.extend_from_slice(chain.to_string().as_bytes());
bytes.extend_from_slice(&address.to_vec());
ServiceId::hash(bytes)
}
}
}
}
impl ServiceManager {
pub fn chain(&self) -> &ChainKey {
match self {
ServiceManager::Evm { chain, .. } => chain,
ServiceManager::Cosmos { chain, .. } => chain,
}
}
pub fn evm_address_unchecked(&self) -> alloy_primitives::Address {
match self {
ServiceManager::Evm { address, .. } => *address,
ServiceManager::Cosmos { .. } => {
panic!("ServiceManager is not EVM type, cannot get EVM address")
}
}
}
pub fn cosmos_address_unchecked(&self) -> layer_climb_address::CosmosAddr {
match self {
ServiceManager::Cosmos { address, .. } => address.clone(),
ServiceManager::Evm { .. } => {
panic!("ServiceManager is not Cosmos type, cannot get Cosmos 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)]
#[serde(rename_all = "snake_case")]
pub enum ComponentSource {
Download {
#[schema(value_type = String)]
uri: UriString,
digest: ComponentDigest,
},
Registry {
#[serde(flatten)]
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 = u64::MAX;
pub const DEFAULT_TIME_LIMIT_SECONDS: u64 = u64::MAX;
}
#[derive(Hash, Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum Trigger {
CosmosContractEvent {
#[schema(value_type = String)]
address: layer_climb_address::CosmosAddr,
chain: ChainKey,
event_type: String,
},
EvmContractEvent {
#[schema(value_type = String)]
address: alloy_primitives::Address,
chain: ChainKey,
event_hash: ByteArray<32>,
},
BlockInterval {
chain: ChainKey,
#[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, ToSchema)]
pub enum TriggerData {
CosmosContractEvent {
#[schema(value_type = String)]
contract_address: layer_climb_address::CosmosAddr,
chain: ChainKey,
#[schema(value_type = Object)]
event: cosmwasm_std::Event,
block_height: u64,
event_index: u64,
},
EvmContractEvent {
chain: ChainKey,
#[schema(value_type = String)]
contract_address: alloy_primitives::Address,
#[schema(value_type = Object)]
log_data: LogData,
#[schema(value_type = String)]
tx_hash: alloy_primitives::TxHash,
block_number: u64,
log_index: u64,
#[schema(value_type = String)]
block_hash: alloy_primitives::B256,
block_timestamp: Option<u64>,
tx_index: u64,
},
BlockInterval {
chain: ChainKey,
block_height: u64,
},
Cron {
trigger_time: Timestamp,
},
Raw(Vec<u8>),
}
impl Default for TriggerData {
fn default() -> Self {
Self::new_raw(vec![])
}
}
impl TriggerData {
pub fn new_raw(data: impl AsRef<[u8]>) -> Self {
TriggerData::Raw(data.as_ref().to_vec())
}
pub fn trigger_type(&self) -> &str {
match self {
TriggerData::CosmosContractEvent { .. } => "cosmos_contract_event",
TriggerData::EvmContractEvent { .. } => "evm_contract_event",
TriggerData::BlockInterval { .. } => "block_interval",
TriggerData::Cron { .. } => "cron",
TriggerData::Raw(_) => "manual",
}
}
pub fn chain(&self) -> Option<&ChainKey> {
match self {
TriggerData::CosmosContractEvent { chain, .. }
| TriggerData::EvmContractEvent { chain, .. }
| TriggerData::BlockInterval { chain, .. } => Some(chain),
TriggerData::Cron { .. } | TriggerData::Raw(_) => None,
}
}
}
#[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: Box<Component>,
signature_kind: SignatureKind,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
pub struct SignatureKind {
pub algorithm: SignatureAlgorithm,
pub prefix: Option<SignaturePrefix>,
}
impl SignatureKind {
pub fn evm_default() -> Self {
Self {
algorithm: SignatureAlgorithm::Secp256k1,
prefix: Some(SignaturePrefix::Eip191),
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SignatureAlgorithm {
Secp256k1,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, ToSchema)]
#[serde(rename_all = "snake_case")]
pub enum SignaturePrefix {
Eip191,
}
#[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 {
#[serde(with = "hex")]
pub payload: Vec<u8>,
pub ordering: Option<u64>,
}
mod test_ext {
use std::{
collections::{BTreeMap, BTreeSet},
num::NonZeroU32,
};
use crate::{
ByteArray, ChainKey, ChainKeyError, ComponentSource, ServiceId, WorkflowId, WorkflowIdError,
};
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::CosmosAddr,
chain: impl TryInto<ChainKey, Error = ChainKeyError>,
event_type: impl ToString,
) -> Self {
Trigger::CosmosContractEvent {
address,
chain: chain.try_into().unwrap(),
event_type: event_type.to_string(),
}
}
pub fn evm_contract_event(
address: alloy_primitives::Address,
chain: impl TryInto<ChainKey, Error = ChainKeyError>,
event_hash: ByteArray<32>,
) -> Self {
Trigger::EvmContractEvent {
address,
chain: chain.try_into().unwrap(),
event_hash,
}
}
}
impl TriggerConfig {
pub fn cosmos_contract_event(
service_id: ServiceId,
workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
contract_address: layer_climb_address::CosmosAddr,
chain: impl TryInto<ChainKey, Error = ChainKeyError>,
event_type: impl ToString,
) -> Self {
Self {
service_id,
workflow_id: workflow_id.try_into().unwrap(),
trigger: Trigger::cosmos_contract_event(contract_address, chain, event_type),
}
}
pub fn evm_contract_event(
service_id: ServiceId,
workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
contract_address: alloy_primitives::Address,
chain: impl TryInto<ChainKey, Error = ChainKeyError>,
event_hash: ByteArray<32>,
) -> Self {
Self {
service_id,
workflow_id: workflow_id.try_into().unwrap(),
trigger: Trigger::evm_contract_event(contract_address, chain, event_hash),
}
}
pub fn block_interval_event(
service_id: ServiceId,
workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
chain: impl TryInto<ChainKey, Error = ChainKeyError>,
n_blocks: NonZeroU32,
) -> Self {
Self {
service_id,
workflow_id: workflow_id.try_into().unwrap(),
trigger: Trigger::BlockInterval {
chain: chain.try_into().unwrap(),
n_blocks,
start_block: None,
end_block: None,
},
}
}
#[cfg(test)]
pub fn manual(
service_id: ServiceId,
workflow_id: impl TryInto<WorkflowId, Error = WorkflowIdError>,
) -> Self {
Self {
service_id,
workflow_id: workflow_id.try_into().unwrap(),
trigger: Trigger::Manual,
}
}
}
}