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