borderless/
common.rs

1use std::{collections::BTreeMap, fmt::Display, str::FromStr};
2
3use borderless_id_types::{AgentId, Uuid};
4use borderless_pkg::WasmPkg;
5use serde::{Deserialize, Serialize};
6use serde_json::Value;
7
8pub use borderless_pkg as pkg;
9
10use crate::{
11    contracts::{Role, TxCtx},
12    events::Sink,
13    BorderlessId, ContractId,
14};
15
16/// High level description and information about the contract or agent itself
17#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
18pub struct Description {
19    pub display_name: String,
20    pub summary: String,
21    #[serde(default)]
22    pub legal: Option<String>,
23}
24
25/// Metadata of the contract or process.
26///
27/// Used for administration purposes.
28#[derive(Debug, Default, Clone, Serialize, Deserialize)]
29pub struct Metadata {
30    #[serde(default)]
31    /// Time when the contract or process was created (milliseconds since unix epoch)
32    pub active_since: u64,
33
34    #[serde(default)]
35    /// Transaction context of the contract-introduction transaction
36    ///
37    /// Is `None`, if the entity is not a contract.
38    pub tx_ctx_introduction: Option<TxCtx>,
39
40    /// Time when the contract or process was revoked or archived (milliseconds since unix epoch)
41    #[serde(default)]
42    pub inactive_since: u64,
43
44    #[serde(default)]
45    /// Transaction context of the contract-revocation transaction (only for contracts)
46    ///
47    /// Is `None`, if the entity is not a contract.
48    pub tx_ctx_revocation: Option<TxCtx>,
49
50    /// Parent of the contract or process (in case the contract / agent was updated or replaced by a newer version)
51    #[serde(default)]
52    pub parent: Option<Uuid>,
53}
54
55/// Generalized ID-Tag for contracts and agents
56#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
57#[serde(untagged)]
58pub enum Id {
59    Contract { contract_id: ContractId },
60    Agent { agent_id: AgentId },
61}
62
63impl Id {
64    pub fn as_cid(&self) -> Option<ContractId> {
65        match self {
66            Id::Contract { contract_id } => Some(*contract_id),
67            Id::Agent { .. } => None,
68        }
69    }
70
71    pub fn as_aid(&self) -> Option<AgentId> {
72        match self {
73            Id::Contract { .. } => None,
74            Id::Agent { agent_id } => Some(*agent_id),
75        }
76    }
77
78    pub fn contract(contract_id: ContractId) -> Self {
79        Id::Contract { contract_id }
80    }
81
82    pub fn agent(agent_id: AgentId) -> Self {
83        Id::Agent { agent_id }
84    }
85}
86
87impl Display for Id {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        match self {
90            Id::Contract { contract_id } => write!(f, "{contract_id}"),
91            Id::Agent { agent_id } => write!(f, "{agent_id}"),
92        }
93    }
94}
95
96impl AsRef<[u8; 16]> for Id {
97    fn as_ref(&self) -> &[u8; 16] {
98        match self {
99            Id::Contract { contract_id } => contract_id.as_ref(),
100            Id::Agent { agent_id } => agent_id.as_ref(),
101        }
102    }
103}
104
105impl PartialEq<ContractId> for Id {
106    fn eq(&self, other: &ContractId) -> bool {
107        match self {
108            Id::Contract { contract_id } => contract_id == other,
109            Id::Agent { .. } => false,
110        }
111    }
112}
113
114impl PartialEq<AgentId> for Id {
115    fn eq(&self, other: &AgentId) -> bool {
116        match self {
117            Id::Agent { agent_id } => agent_id == other,
118            Id::Contract { .. } => false,
119        }
120    }
121}
122
123impl From<ContractId> for Id {
124    fn from(contract_id: ContractId) -> Self {
125        Id::Contract { contract_id }
126    }
127}
128
129impl From<AgentId> for Id {
130    fn from(agent_id: AgentId) -> Self {
131        Id::Agent { agent_id }
132    }
133}
134
135// NOTE: We could re-write the participant logic like this
136//
137// But that's maybe something for later.
138//
139// pub struct Participant {
140//     pub borderless_id: BorderlessId,
141//     pub alias: String,
142//     pub roles: Vec<String>,
143//     pub sinks: Vec<String>,
144// }
145// { "borderless-id": "4bec7f8e-5074-49a5-9b94-620fb13f12c0", "alias": null, roles": [ "Flipper" ], "sinks": [ "OTHERFLIPPER" ] },
146
147/*
148 * Ok, spitballing here:
149 *
150 * I think the sinks as they are now, are quite OK.
151 * The only thing I would change is, that the sinks that the contract itself defines (with the enum),
152 * should work differently in the way that they just output their data as plain json,
153 * and the sinks (enum below) are used to subscribe to those outputs using the "alias".
154 * We should add a "MethodOrId" to each sink; then we are able to build the CallAction struct for the corresponding
155 * contract or agent.
156 */
157
158/// An introduction of either a contract or agent
159///
160/// There are no two distinct types, since the similarities between contracts and agents are quite big.
161/// The main difference is, that agents have no roles attached to them and are not introduced or revoked by a transaction.
162#[derive(Debug, Clone, Serialize, Deserialize)]
163pub struct Introduction {
164    /// Contract- or Agent-ID
165    #[serde(flatten)]
166    pub id: Id,
167
168    /// List of participants
169    #[serde(default)]
170    pub participants: Vec<BorderlessId>,
171
172    /// Initial state as JSON value
173    ///
174    /// This will be parsed by the implementors of the contract or agent
175    pub initial_state: Value,
176
177    /// Mapping between users and roles.
178    ///
179    /// Only relevant for contracts
180    #[serde(default)]
181    pub roles: Vec<Role>,
182
183    /// List of available sinks
184    #[serde(default)]
185    pub sinks: Vec<Sink>,
186
187    /// High-Level description of the contract or agent
188    pub desc: Description,
189
190    #[serde(default)]
191    /// metadata of the contract or agent
192    pub meta: Metadata,
193
194    /// Definition of the wasm package for this contract or agent
195    pub package: WasmPkg,
196}
197
198impl Introduction {
199    /// Encode the introduction to json bytes
200    pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
201        serde_json::to_vec(&self)
202    }
203
204    /// Decode the introduction from json bytes
205    pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
206        serde_json::from_slice(bytes)
207    }
208
209    /// Pretty-Print the introduction as json
210    pub fn pretty_print(&self) -> Result<String, serde_json::Error> {
211        serde_json::to_string_pretty(&self)
212    }
213}
214
215impl FromStr for Introduction {
216    type Err = serde_json::Error;
217
218    fn from_str(s: &str) -> Result<Self, Self::Err> {
219        serde_json::from_str(s)
220    }
221}
222
223/// Digital-Tranfer-Object (Dto) of an [`Introduction`]
224///
225/// When new contracts or agents are created via web-api, things like [`Metadata`] do not make sense yet.
226/// This DTO omits the metadata and makes the [`Id`] optional, so a new [`Id`] can be generated for the package.
227#[derive(Debug, Clone, Serialize, Deserialize)]
228pub struct IntroductionDto {
229    /// Optional Contract- or Agent-ID
230    ///
231    /// If this field is empty, a new ID will be generated for the contract or agent.
232    #[serde(flatten)]
233    #[serde(default)]
234    #[serde(skip_serializing_if = "Option::is_none")]
235    pub id: Option<Id>,
236
237    /// List of participants
238    #[serde(default)]
239    #[serde(skip_serializing_if = "Vec::is_empty")]
240    pub participants: Vec<BorderlessId>,
241
242    /// Initial state as JSON value
243    ///
244    /// This will be parsed by the implementors of the contract or agent
245    pub initial_state: Value,
246
247    /// Mapping between users and roles.
248    ///
249    /// Only relevant for contracts
250    #[serde(default)]
251    #[serde(skip_serializing_if = "Vec::is_empty")]
252    pub roles: Vec<Role>,
253
254    /// List of available sinks
255    #[serde(default)]
256    #[serde(skip_serializing_if = "Vec::is_empty")]
257    pub sinks: Vec<Sink>,
258
259    /// High-Level description of the contract or agent
260    pub desc: Description,
261
262    /// Definition of the wasm package for this contract or agent
263    pub package: WasmPkg,
264}
265
266// TODO: Implement conversion from DTO to Introduction
267
268/// Contract revocation
269#[derive(Debug, Clone, Serialize, Deserialize)]
270pub struct Revocation {
271    /// Contract- or Agent-ID
272    #[serde(flatten)]
273    pub id: Id,
274
275    /// Reason for the revocation
276    pub reason: String,
277}
278
279impl Revocation {
280    /// Encode the revocation to json bytes
281    pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
282        serde_json::to_vec(&self)
283    }
284
285    /// Decode the revocation from json bytes
286    pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
287        serde_json::from_slice(bytes)
288    }
289
290    /// Pretty-Print the revocation as json
291    pub fn pretty_print(&self) -> Result<String, serde_json::Error> {
292        serde_json::to_string_pretty(&self)
293    }
294}
295
296impl FromStr for Revocation {
297    type Err = serde_json::Error;
298
299    fn from_str(s: &str) -> Result<Self, Self::Err> {
300        serde_json::from_str(s)
301    }
302}
303
304/// Generated symbols of a contract
305#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct Symbols {
307    /// Fields and addresses (storage-keys) of the contract-state
308    pub state: BTreeMap<String, u64>,
309    /// Method-names and method-ids of all actions
310    pub actions: BTreeMap<String, u32>,
311}
312
313impl Symbols {
314    // TODO: I liked the hex-encoding more, but it also made it harder to debug based on the generated symbols in the contract.
315    //
316    // We should either use hex everywhere or the raw number everywhere. For now I will use the numbers here, but I would maybe change
317    // the macro later to utilize the hex-encoding.
318    pub fn from_symbols(state_syms: &[(&str, u64)], action_syms: &[(&str, u32)]) -> Self {
319        // NOTE: We use a BTreeMap instead of a hash-map to get sorted keys.
320        let mut state = BTreeMap::new();
321        for (name, addr) in state_syms {
322            state.insert(name.to_string(), *addr);
323        }
324        let mut actions = BTreeMap::new();
325        for (name, addr) in action_syms {
326            actions.insert(name.to_string(), *addr);
327        }
328        Self { state, actions }
329    }
330
331    /// Use json to encode the `Symbols`
332    pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
333        serde_json::to_vec(self)
334    }
335
336    /// Use json to decode the `Symbols`
337    pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
338        serde_json::from_slice(bytes)
339    }
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn general_id() {
348        let cid = r#"{ "contract_id": "cbcd81bb-b90c-8806-8341-fe95b8ede45a" }"#;
349        let aid = r#"{ "agent_id": "abcd81bb-b90c-8806-8341-fe95b8ede45a" }"#;
350        let parsed: Result<Id, _> = serde_json::from_str(&cid);
351        assert!(parsed.is_ok(), "{}", parsed.unwrap_err());
352        match parsed.unwrap() {
353            Id::Contract { contract_id } => assert_eq!(
354                contract_id.to_string(),
355                "cbcd81bb-b90c-8806-8341-fe95b8ede45a"
356            ),
357            Id::Agent { .. } => panic!("result was not an agent-id"),
358        }
359
360        let parsed: Result<Id, _> = serde_json::from_str(&aid);
361        assert!(parsed.is_ok(), "{}", parsed.unwrap_err());
362        match parsed.unwrap() {
363            Id::Agent { agent_id } => {
364                assert_eq!(agent_id.to_string(), "abcd81bb-b90c-8806-8341-fe95b8ede45a")
365            }
366            Id::Contract { .. } => panic!("result was not a contract-id"),
367        }
368    }
369
370    #[test]
371    fn parse_introduction() {
372        let json = r#"
373{
374  "contract_id": "cc8ca79c-3bbb-89d2-bb28-29636c170387",
375  "participants": [],
376  "initial_state": {
377    "switch": true,
378    "counter": 0,
379    "history": []
380  },
381  "roles": [],
382  "sinks": [],
383  "desc": {
384    "display_name": "flipper",
385    "summary": "a flipper contract for testing the abi",
386    "legal": null
387  },
388  "meta": {},
389  "package": {
390     "name": "flipper-contract",
391     "pkg_type": "contract",
392     "source": {
393        "version": "0.1.0",
394        "digest": "",
395        "wasm": ""
396     }
397  }
398}
399"#;
400        let result: Result<Introduction, _> = serde_json::from_str(&json);
401        assert!(result.is_ok(), "{}", result.unwrap_err());
402        let introduction = result.unwrap();
403        assert_eq!(
404            introduction.id,
405            Id::Contract {
406                contract_id: "cc8ca79c-3bbb-89d2-bb28-29636c170387".parse().unwrap()
407            }
408        );
409        let json = json.replace(r#""contract_id": "c"#, r#""agent_id": "a"#);
410        let result: Result<Introduction, _> = serde_json::from_str(&json);
411        assert!(result.is_ok(), "{}", result.unwrap_err());
412        let introduction = result.unwrap();
413        assert_eq!(
414            introduction.id,
415            Id::Agent {
416                agent_id: "ac8ca79c-3bbb-89d2-bb28-29636c170387".parse().unwrap()
417            }
418        );
419    }
420}