use anyhow::anyhow;
use borderless_id_types::{AgentId, Uuid};
use borderless_pkg::{PkgType, WasmPkg};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::{collections::BTreeMap, fmt::Display, str::FromStr};
#[cfg(not(feature = "generate_ids"))]
use anyhow::Context;
pub use borderless_pkg as pkg;
use crate::{contracts::TxCtx, events::Sink, events::Topic, BorderlessId, ContractId};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Description {
pub display_name: String,
pub summary: String,
#[serde(default)]
pub legal: Option<String>,
}
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct Metadata {
#[serde(default)]
pub active_since: u64,
#[serde(default)]
pub tx_ctx_introduction: Option<TxCtx>,
#[serde(default)]
pub inactive_since: u64,
#[serde(default)]
pub tx_ctx_revocation: Option<TxCtx>,
#[serde(default)]
pub parent: Option<Uuid>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum Id {
Contract { contract_id: ContractId },
Agent { agent_id: AgentId },
}
impl Id {
pub fn as_cid(&self) -> Option<ContractId> {
match self {
Id::Contract { contract_id } => Some(*contract_id),
Id::Agent { .. } => None,
}
}
pub fn as_aid(&self) -> Option<AgentId> {
match self {
Id::Contract { .. } => None,
Id::Agent { agent_id } => Some(*agent_id),
}
}
pub fn contract(contract_id: ContractId) -> Self {
Id::Contract { contract_id }
}
pub fn agent(agent_id: AgentId) -> Self {
Id::Agent { agent_id }
}
#[cfg(feature = "generate_ids")]
pub fn generate(pkg_type: &borderless_pkg::PkgType) -> Self {
match pkg_type {
borderless_pkg::PkgType::Contract => Id::Contract {
contract_id: ContractId::generate(),
},
borderless_pkg::PkgType::Agent => Id::Agent {
agent_id: AgentId::generate(),
},
}
}
}
impl Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Id::Contract { contract_id } => write!(f, "{contract_id}"),
Id::Agent { agent_id } => write!(f, "{agent_id}"),
}
}
}
impl AsRef<[u8; 16]> for Id {
fn as_ref(&self) -> &[u8; 16] {
match self {
Id::Contract { contract_id } => contract_id.as_ref(),
Id::Agent { agent_id } => agent_id.as_ref(),
}
}
}
impl PartialEq<ContractId> for Id {
fn eq(&self, other: &ContractId) -> bool {
match self {
Id::Contract { contract_id } => contract_id == other,
Id::Agent { .. } => false,
}
}
}
impl PartialEq<AgentId> for Id {
fn eq(&self, other: &AgentId) -> bool {
match self {
Id::Agent { agent_id } => agent_id == other,
Id::Contract { .. } => false,
}
}
}
impl From<ContractId> for Id {
fn from(contract_id: ContractId) -> Self {
Id::Contract { contract_id }
}
}
impl From<AgentId> for Id {
fn from(agent_id: AgentId) -> Self {
Id::Agent { agent_id }
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Participant {
pub id: BorderlessId,
pub alias: String,
#[serde(default)]
pub roles: Vec<String>,
}
impl Participant {
pub fn add_alias_to_roles(&mut self) {
if !self.roles.contains(&self.alias) {
self.roles.push(self.alias.clone());
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Introduction {
#[serde(flatten)]
pub id: Id,
#[serde(default)]
pub participants: Vec<Participant>,
pub initial_state: Value,
#[serde(default)]
pub sinks: Vec<Sink>,
#[serde(default)]
pub subscriptions: Vec<Topic>,
pub desc: Description,
#[serde(default)]
pub meta: Metadata,
pub package: WasmPkg,
}
impl Introduction {
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec(&self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
pub fn pretty_print(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self)
}
}
impl FromStr for Introduction {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct IntroductionDto {
#[serde(flatten)]
#[serde(default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Id>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub participants: Vec<Participant>,
pub initial_state: Value,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub sinks: Vec<SinkDto>,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub subscriptions: Vec<Topic>,
pub desc: Description,
pub package: WasmPkg,
}
impl TryFrom<IntroductionDto> for Introduction {
type Error = crate::Error;
fn try_from(value: IntroductionDto) -> Result<Self, Self::Error> {
let id = {
#[cfg(feature = "generate_ids")]
{
Id::generate(&value.package.pkg_type)
}
#[cfg(not(feature = "generate_ids"))]
{
value.id.with_context(|| {
"ID must be set - enable feature 'generate_ids' to autogenerate an ID"
})?
}
};
let valid = match value.package.pkg_type {
PkgType::Contract => id.as_cid().is_some(),
PkgType::Agent => id.as_aid().is_some(),
};
if !valid {
return Err(anyhow!("Mismatch between provided ID and package type"));
}
let sinks: Vec<Sink> = value.sinks.into_iter().map(|s| s.into()).collect();
match id {
Id::Contract { .. } => {
if sinks.iter().any(|s| s.writer.is_empty()) {
return Err(anyhow!(
"Sinks defined in a SmartContract must contain a writer"
));
}
if value.participants.is_empty() {
return Err(anyhow!("SmartContracts must contain participants"));
}
if !value.subscriptions.is_empty() {
return Err(anyhow!("SmartContracts do not support subscriptions"));
}
}
Id::Agent { .. } => {
if sinks.iter().any(|s| !s.writer.is_empty()) {
return Err(anyhow!(
"Sinks defined in a sw-agent must NOT contain a writer"
));
}
if !value.participants.is_empty() {
return Err(anyhow!("Sw-Agents must NOT contain participants"));
}
}
}
Ok(Self {
id,
participants: value.participants,
initial_state: value.initial_state,
sinks,
subscriptions: value.subscriptions,
desc: value.desc,
meta: Default::default(),
package: value.package,
})
}
}
impl FromStr for IntroductionDto {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SinkDto {
pub contract_id: ContractId,
pub alias: String,
pub writer: Option<String>,
}
impl From<SinkDto> for Sink {
fn from(value: SinkDto) -> Self {
Self {
contract_id: value.contract_id,
alias: value.alias,
writer: value.writer.unwrap_or_default(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Revocation {
#[serde(flatten)]
pub id: Id,
pub reason: String,
}
impl Revocation {
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec(&self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
pub fn pretty_print(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self)
}
}
impl FromStr for Revocation {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Symbols {
pub state: BTreeMap<String, u64>,
pub actions: BTreeMap<String, u32>,
}
impl Symbols {
pub fn from_symbols(state_syms: &[(&str, u64)], action_syms: &[(&str, u32)]) -> Self {
let mut state = BTreeMap::new();
for (name, addr) in state_syms {
state.insert(name.to_string(), *addr);
}
let mut actions = BTreeMap::new();
for (name, addr) in action_syms {
actions.insert(name.to_string(), *addr);
}
Self { state, actions }
}
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
serde_json::to_vec(self)
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
serde_json::from_slice(bytes)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn general_id() {
let cid = r#"{ "contract_id": "cbcd81bb-b90c-8806-8341-fe95b8ede45a" }"#;
let aid = r#"{ "agent_id": "abcd81bb-b90c-8806-8341-fe95b8ede45a" }"#;
let parsed: Result<Id, _> = serde_json::from_str(cid);
assert!(parsed.is_ok(), "{}", parsed.unwrap_err());
match parsed.unwrap() {
Id::Contract { contract_id } => assert_eq!(
contract_id.to_string(),
"cbcd81bb-b90c-8806-8341-fe95b8ede45a"
),
Id::Agent { .. } => panic!("result was not an agent-id"),
}
let parsed: Result<Id, _> = serde_json::from_str(aid);
assert!(parsed.is_ok(), "{}", parsed.unwrap_err());
match parsed.unwrap() {
Id::Agent { agent_id } => {
assert_eq!(agent_id.to_string(), "abcd81bb-b90c-8806-8341-fe95b8ede45a")
}
Id::Contract { .. } => panic!("result was not a contract-id"),
}
}
#[test]
fn parse_introduction() {
let json = r#"
{
"contract_id": "cc8ca79c-3bbb-89d2-bb28-29636c170387",
"participants": [],
"initial_state": {
"switch": true,
"counter": 0,
"history": []
},
"roles": [],
"sinks": [],
"desc": {
"display_name": "flipper",
"summary": "a flipper contract for testing the abi",
"legal": null
},
"meta": {},
"package": {
"name": "flipper-contract",
"pkg_type": "contract",
"source": {
"version": "0.1.0",
"digest": "",
"wasm": ""
}
}
}
"#;
let result: Result<Introduction, _> = serde_json::from_str(json);
assert!(result.is_ok(), "{}", result.unwrap_err());
let introduction = result.unwrap();
assert_eq!(
introduction.id,
Id::Contract {
contract_id: "cc8ca79c-3bbb-89d2-bb28-29636c170387".parse().unwrap()
}
);
let json = json.replace(r#""contract_id": "c"#, r#""agent_id": "a"#);
let result: Result<Introduction, _> = serde_json::from_str(&json);
assert!(result.is_ok(), "{}", result.unwrap_err());
let introduction = result.unwrap();
assert_eq!(
introduction.id,
Id::Agent {
agent_id: "ac8ca79c-3bbb-89d2-bb28-29636c170387".parse().unwrap()
}
);
}
}