odra_schema/
lib.rs

1//! A module providing functionality for defining the Casper Contract Schema.
2//!
3//! It includes traits for defining entrypoints, events, custom types, and errors, as well as functions
4//! for creating various schema elements such as arguments, entrypoints, struct members, enum variants, custom types,
5//! events, and errors.
6use std::{collections::BTreeSet, env, path::PathBuf};
7
8pub use casper_contract_schema;
9use casper_contract_schema::{
10    Access, Argument, CallMethod, ContractSchema, CustomType, Entrypoint, EnumVariant, Event,
11    NamedCLType, StructMember, UserError
12};
13
14use odra_core::args::EntrypointArgument;
15
16const CCSV: u8 = 1;
17
18mod custom_type;
19mod ty;
20
21pub use ty::NamedCLTyped;
22
23/// Trait representing schema entrypoints.
24pub trait SchemaEntrypoints {
25    /// Returns a vector of [Entrypoint]s.
26    fn schema_entrypoints() -> Vec<Entrypoint>;
27}
28
29/// Trait representing schema events.
30pub trait SchemaEvents {
31    /// Returns a vector of [Event]s.
32    fn schema_events() -> Vec<Event> {
33        vec![]
34    }
35
36    /// Returns a vector of [CustomType]s.
37    ///
38    /// This method is used to define custom types that are used in the events.
39    /// An event itself is a [CustomType] and can have a custom type as its payload.
40    fn custom_types() -> Vec<Option<CustomType>> {
41        vec![]
42    }
43}
44
45/// Trait for defining custom types in a schema.
46pub trait SchemaCustomTypes {
47    /// Returns a vector of optional [CustomType]s.
48    fn schema_types() -> Vec<Option<CustomType>> {
49        vec![]
50    }
51}
52
53/// A trait for defining schema user errors.
54pub trait SchemaErrors {
55    /// Returns a vector of [UserError]s.
56    fn schema_errors() -> Vec<UserError> {
57        vec![]
58    }
59}
60
61/// Represents a custom element in the schema.
62pub trait SchemaCustomElement {}
63
64impl<T: SchemaCustomElement> SchemaErrors for T {}
65impl<T: SchemaCustomElement> SchemaEvents for T {}
66
67/// Creates a new argument.
68pub fn argument<T: NamedCLTyped + EntrypointArgument>(name: &str) -> Argument {
69    if T::is_required() {
70        Argument::new(name, "", <T as NamedCLTyped>::ty())
71    } else {
72        Argument::new_opt(name, "", <T as NamedCLTyped>::ty())
73    }
74}
75
76/// Creates a new entrypoint.
77pub fn entry_point<T: NamedCLTyped>(
78    name: &str,
79    description: &str,
80    is_mutable: bool,
81    arguments: Vec<Argument>
82) -> Entrypoint {
83    Entrypoint {
84        name: name.into(),
85        description: Some(description.to_string()),
86        is_mutable,
87        arguments,
88        return_ty: T::ty().into(),
89        is_contract_context: true,
90        access: Access::Public
91    }
92}
93
94/// Creates a new struct member.
95pub fn struct_member<T: NamedCLTyped>(name: &str) -> StructMember {
96    StructMember {
97        name: name.to_string(),
98        description: None,
99        ty: T::ty().into()
100    }
101}
102
103/// Creates a new enum variant.
104pub fn enum_typed_variant<T: NamedCLTyped>(name: &str, discriminant: u16) -> EnumVariant {
105    EnumVariant {
106        name: name.to_string(),
107        description: None,
108        discriminant,
109        ty: T::ty().into()
110    }
111}
112
113/// Creates a new enum variant of type [NamedCLType::Unit].
114pub fn enum_variant(name: &str, discriminant: u16) -> EnumVariant {
115    enum_typed_variant::<()>(name, discriminant)
116}
117
118/// Creates a new enum variant of type [NamedCLType::Custom].
119pub fn enum_custom_type_variant(name: &str, discriminant: u16, custom_type: &str) -> EnumVariant {
120    EnumVariant {
121        name: name.to_string(),
122        description: None,
123        discriminant,
124        ty: NamedCLType::Custom(custom_type.into()).into()
125    }
126}
127
128/// Creates a new [CustomType] of type struct.
129pub fn custom_struct(name: &str, members: Vec<StructMember>) -> CustomType {
130    CustomType::Struct {
131        name: name.into(),
132        description: None,
133        members
134    }
135}
136
137/// Creates a new [CustomType] of type enum.
138pub fn custom_enum(name: &str, variants: Vec<EnumVariant>) -> CustomType {
139    CustomType::Enum {
140        name: name.into(),
141        description: None,
142        variants
143    }
144}
145
146/// Creates a new [Event].  
147pub fn event(name: &str) -> Event {
148    Event {
149        name: name.into(),
150        ty: name.into()
151    }
152}
153
154/// Creates a new [UserError].
155pub fn error(name: &str, description: &str, discriminant: u16) -> UserError {
156    UserError {
157        name: name.into(),
158        description: Some(description.into()),
159        discriminant
160    }
161}
162
163/// Creates an instance of [ContractSchema].
164///
165/// A contract schema is a representation of a smart contract's schema. It includes information about
166/// the contract's metadata, entrypoints, events, custom types, and errors.
167pub fn schema<T: SchemaEntrypoints + SchemaEvents + SchemaCustomTypes + SchemaErrors>(
168    module_name: &str,
169    contract_name: &str,
170    contract_version: &str,
171    authors: Vec<String>,
172    repository: &str,
173    homepage: &str
174) -> ContractSchema {
175    let entry_points = T::schema_entrypoints();
176    let events = T::schema_events();
177    let errors = T::schema_errors();
178    let types = BTreeSet::from_iter(T::schema_types())
179        .into_iter()
180        .flatten()
181        .collect();
182
183    let init_ep = entry_points.iter().find(|e| e.name == "init");
184
185    let init_args = init_ep.map(|e| e.arguments.clone()).unwrap_or_default();
186
187    let init_description = init_ep.and_then(|e| e.description.clone());
188
189    let entry_points = entry_points
190        .into_iter()
191        .filter(|e| e.name != "init")
192        .collect();
193
194    let wasm_file_name = format!("{}.wasm", module_name);
195
196    let repository = match repository {
197        "" => None,
198        _ => Some(repository.to_string())
199    };
200
201    let homepage = match homepage {
202        "" => None,
203        _ => Some(homepage.to_string())
204    };
205
206    ContractSchema {
207        casper_contract_schema_version: CCSV,
208        toolchain: env!("RUSTC_VERSION").to_string(),
209        contract_name: contract_name.to_string(),
210        contract_version: contract_version.to_string(),
211        types,
212        entry_points,
213        events,
214        call: Some(call_method(wasm_file_name, init_description, &init_args)),
215        authors,
216        repository,
217        homepage,
218        errors
219    }
220}
221
222/// Finds the path to the schema file for the given contract name.
223pub fn find_schema_file_path(
224    contract_name: &str,
225    root_path: PathBuf
226) -> Result<PathBuf, &'static str> {
227    let mut path = root_path
228        .join(format!("{}_schema.json", contract_name.to_lowercase()))
229        .with_extension("json");
230
231    let mut checked_paths = vec![];
232    for _ in 0..2 {
233        if path.exists() && path.is_file() {
234            return Ok(path);
235        } else {
236            checked_paths.push(path.clone());
237            path = path.parent().unwrap().to_path_buf();
238        }
239    }
240    Err("Schema not found")
241}
242
243fn call_method(
244    file_name: String,
245    description: Option<String>,
246    constructor_args: &[Argument]
247) -> CallMethod {
248    CallMethod {
249        wasm_file_name: file_name.to_string(),
250        description: description.map(String::from),
251        arguments: vec![
252            Argument {
253                name: odra_core::consts::PACKAGE_HASH_KEY_NAME_ARG.to_string(),
254                description: Some("The arg name for the package hash key name.".to_string()),
255                ty: NamedCLType::String.into(),
256                optional: false
257            },
258            Argument {
259                name: odra_core::consts::ALLOW_KEY_OVERRIDE_ARG.to_string(),
260                description: Some("If true and the key specified in odra_cfg_package_hash_key_name already exists, it will be overwritten.".to_string()),
261                ty: NamedCLType::Bool.into(),
262                optional: false
263            },
264            Argument {
265                name: odra_core::consts::IS_UPGRADABLE_ARG.to_string(),
266                description: Some(
267                    "The arg name for the contract upgradeability setting.".to_string()
268                ),
269                ty: NamedCLType::Bool.into(),
270                optional: false
271            },
272        ]
273        .iter()
274        .chain(constructor_args.iter())
275        .cloned()
276        .collect()
277    }
278}
279
280#[cfg(test)]
281mod test {
282    use odra_core::args::Maybe;
283    use odra_core::prelude::Address;
284
285    use super::*;
286
287    #[test]
288    fn test_argument() {
289        let arg = super::argument::<u32>("arg1");
290        assert_eq!(arg.name, "arg1");
291        assert_eq!(arg.ty, casper_contract_schema::NamedCLType::U32.into());
292    }
293
294    #[test]
295    fn test_opt_argument() {
296        let arg = super::argument::<Maybe<u32>>("arg1");
297        assert_eq!(arg.name, "arg1");
298        assert_eq!(arg.ty, casper_contract_schema::NamedCLType::U32.into());
299    }
300
301    #[test]
302    fn test_entry_point() {
303        let arg = super::argument::<u32>("arg1");
304        let entry_point = super::entry_point::<u32>("entry1", "description", true, vec![arg]);
305        assert_eq!(entry_point.name, "entry1");
306        assert_eq!(entry_point.description, Some("description".to_string()));
307        assert!(entry_point.is_mutable);
308        assert_eq!(entry_point.arguments.len(), 1);
309        assert_eq!(
310            entry_point.return_ty,
311            casper_contract_schema::NamedCLType::U32.into()
312        );
313    }
314
315    #[test]
316    fn test_struct_member() {
317        let member = super::struct_member::<u32>("member1");
318        assert_eq!(member.name, "member1");
319        assert_eq!(member.ty, casper_contract_schema::NamedCLType::U32.into());
320    }
321
322    #[test]
323    fn test_enum_typed_variant() {
324        let variant = super::enum_typed_variant::<Address>("variant1", 1);
325        assert_eq!(variant.name, "variant1");
326        assert_eq!(variant.discriminant, 1);
327        assert_eq!(variant.ty, casper_contract_schema::NamedCLType::Key.into());
328    }
329
330    #[test]
331    fn test_enum_variant() {
332        let variant = super::enum_variant("variant1", 1);
333        assert_eq!(variant.name, "variant1");
334        assert_eq!(variant.discriminant, 1);
335        assert_eq!(variant.ty, casper_contract_schema::NamedCLType::Unit.into());
336    }
337
338    #[test]
339    fn test_custom_struct() {
340        let member = super::struct_member::<u32>("member1");
341        let custom_struct = super::custom_struct("struct1", vec![member]);
342        match custom_struct {
343            casper_contract_schema::CustomType::Struct { name, members, .. } => {
344                assert_eq!(name, "struct1".into());
345                assert_eq!(members.len(), 1);
346            }
347            _ => panic!("Expected CustomType::Struct")
348        }
349    }
350
351    #[test]
352    fn test_custom_enum() {
353        let variant1 = super::enum_variant("variant1", 1);
354        let variant2 = super::enum_typed_variant::<String>("v2", 2);
355        let variant3 = super::enum_custom_type_variant("v3", 3, "Type1");
356        let custom_enum = super::custom_enum("enum1", vec![variant1, variant2, variant3]);
357        match custom_enum {
358            casper_contract_schema::CustomType::Enum { name, variants, .. } => {
359                assert_eq!(name, "enum1".into());
360                assert_eq!(variants.len(), 3);
361                assert_eq!(variants[0].ty, NamedCLType::Unit.into());
362                assert_eq!(variants[1].ty, NamedCLType::String.into());
363                assert_eq!(variants[2].ty, NamedCLType::Custom("Type1".into()).into());
364            }
365            _ => panic!("Expected CustomType::Enum")
366        }
367    }
368
369    #[test]
370    fn test_event() {
371        let event = super::event("event1");
372        assert_eq!(event.name, "event1");
373    }
374
375    #[test]
376    fn test_error() {
377        let error = super::error("error1", "description", 1);
378        assert_eq!(error.name, "error1");
379        assert_eq!(error.description, Some("description".to_string()));
380        assert_eq!(error.discriminant, 1);
381    }
382
383    #[test]
384    fn test_schema() {
385        struct TestSchema;
386
387        impl SchemaEntrypoints for TestSchema {
388            fn schema_entrypoints() -> Vec<Entrypoint> {
389                vec![entry_point::<u32>(
390                    "entry1",
391                    "description",
392                    true,
393                    vec![super::argument::<u32>("arg1")]
394                )]
395            }
396        }
397
398        impl SchemaEvents for TestSchema {
399            fn schema_events() -> Vec<Event> {
400                vec![event("event1")]
401            }
402        }
403
404        impl SchemaCustomTypes for TestSchema {
405            fn schema_types() -> Vec<Option<CustomType>> {
406                vec![
407                    Some(custom_struct(
408                        "struct1",
409                        vec![struct_member::<u32>("member1")]
410                    )),
411                    Some(custom_enum("struct1", vec![enum_variant("variant1", 1)])),
412                ]
413            }
414        }
415
416        impl SchemaErrors for TestSchema {
417            fn schema_errors() -> Vec<UserError> {
418                vec![]
419            }
420        }
421
422        let schema = super::schema::<TestSchema>(
423            "module_name",
424            "contract_name",
425            "contract_version",
426            vec!["author".to_string()],
427            "repository",
428            "homepage"
429        );
430
431        assert_eq!(schema.contract_name, "contract_name");
432        assert_eq!(schema.contract_version, "contract_version");
433        assert_eq!(schema.authors, vec!["author".to_string()]);
434        assert_eq!(schema.repository, Some("repository".to_string()));
435        assert_eq!(schema.homepage, Some("homepage".to_string()));
436        assert_eq!(schema.entry_points.len(), 1);
437        assert_eq!(schema.types.len(), 2);
438        assert_eq!(schema.errors.len(), 0);
439        assert_eq!(schema.events.len(), 1);
440    }
441}