Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handling optional entry point argument #380

Merged
merged 5 commits into from
Mar 21, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,15 @@

Changelog for `odra`.

## [0.9.0] - 2024-XX-XX
## [0.8.2] - 2024-03-xx
### Added
- `Maybe<T>` - a type that represents an entrypoint arg that may or may not be present.
- `EntrypointArgument` - a trait for types that can be used as entrypoint arguments.
- an example of using `Maybe<T>` in `odra-examples` crate.
- `get_opt_named_arg_bytes(&str)` in `ContractEnv`
- `odra::contract_def::Argument` has a new `is_required` field.

## [0.8.1] - 2024-03-01
### Added
- `ContractRef` trait with `new` and `address` functions. All contract references now implement it.
- `disable-allocator` feature for `odra` crate. It allows to disable the allocator used by Odra Framework in
Expand All @@ -11,7 +19,7 @@ wasm build.
### Changed
- Traits implemented by modules are now also implemented by their `ContractRefs` and `HostRefs`.

## [0.8.0] - 2024-XX-XX
## [0.8.0] - 2024-02-06

### Changed
- Replaced `contract_env::` with `self.env()` in the contract context (of type `ContractEnv`).
Expand Down
218 changes: 218 additions & 0 deletions core/src/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
//! This module provides types and traits for working with entrypoint arguments.

use crate::{contract_def::Argument, prelude::*, ContractEnv, ExecutionError};
use casper_types::{
bytesrepr::{FromBytes, ToBytes},
CLType, CLTyped, Parameter, RuntimeArgs
};

/// A type that represents an entrypoint arg that may or may not be present.
#[derive(Debug, Clone)]
pub enum Maybe<T> {
/// A value is present.
Some(T),
/// No value is present.
None
}

impl<T> Maybe<T> {
/// Returns `true` if the value is present.
pub fn is_some(&self) -> bool {
matches!(self, Maybe::Some(_))
}

/// Returns `true` if the value is not present.
pub fn is_none(&self) -> bool {
matches!(self, Maybe::None)
}

/// Unwraps the value.
/// If the value is not present, the contract reverts with an `ExecutionError::UnwrapError`.
pub fn unwrap(self, env: &ContractEnv) -> T {
match self {
Maybe::Some(value) => value,
Maybe::None => env.revert(ExecutionError::UnwrapError)
}
}
}

impl<T: Default> Maybe<T> {
/// Unwraps the value or returns the default value.
pub fn unwrap_or_default(self) -> T {
match self {
Maybe::Some(value) => value,
Maybe::None => T::default()
}
}
}

impl<T: ToBytes> ToBytes for Maybe<T> {
fn to_bytes(&self) -> Result<Vec<u8>, casper_types::bytesrepr::Error> {
match self {
Maybe::Some(value) => value.to_bytes(),
Maybe::None => Ok(Vec::new())
}
}

fn serialized_length(&self) -> usize {
match self {
Maybe::Some(value) => value.serialized_length(),
Maybe::None => 0
}
}
}

impl<T: FromBytes> FromBytes for Maybe<T> {
fn from_bytes(bytes: &[u8]) -> Result<(Self, &[u8]), casper_types::bytesrepr::Error> {
let res = T::from_bytes(bytes);
if let Ok((value, rem)) = res {
Ok((Maybe::Some(value), rem))
} else {
Ok((Maybe::None, bytes))
}
}

fn from_vec(bytes: Vec<u8>) -> Result<(Self, Vec<u8>), casper_types::bytesrepr::Error> {
Self::from_bytes(bytes.as_slice()).map(|(x, remainder)| (x, Vec::from(remainder)))
}
}

/// A trait for types that can be used as entrypoint arguments.
pub trait EntrypointArgument: Sized {
/// Returns `true` if the argument is required.
fn is_required() -> bool;
/// Returns the CLType of the argument.
fn cl_type() -> CLType;
/// Inserts the argument into the runtime args.
fn insert_runtime_arg(self, name: &str, args: &mut RuntimeArgs);
/// Unwraps the argument from an Option.
fn unwrap(value: Option<Self>, env: &ContractEnv) -> Self;
}

impl<T: CLTyped + ToBytes> EntrypointArgument for Maybe<T> {
fn is_required() -> bool {
false
}

fn cl_type() -> CLType {
T::cl_type()
}

fn insert_runtime_arg(self, name: &str, args: &mut RuntimeArgs) {
if let Maybe::Some(v) = self {
let _ = args.insert(name, v);
}
}

fn unwrap(value: Option<Self>, _env: &ContractEnv) -> Self {
kpob marked this conversation as resolved.
Show resolved Hide resolved
match value {
Some(v) => v,
None => Maybe::None
}
}
}

impl<T: CLTyped + ToBytes> EntrypointArgument for T {
fn is_required() -> bool {
true
}

fn cl_type() -> CLType {
T::cl_type()
}

fn insert_runtime_arg(self, name: &str, args: &mut RuntimeArgs) {
let _ = args.insert(name, self);
}

fn unwrap(value: Option<Self>, env: &ContractEnv) -> Self {
match value {
Some(v) => v,
None => env.revert(ExecutionError::UnwrapError)
}
}
}

/// Returns a Casper entrypoint argument representation.
/// If the parameter is not required, it returns `None`.
pub fn parameter<T: EntrypointArgument>(name: &str) -> Option<Parameter> {
match T::is_required() {
true => Some(Parameter::new(name, T::cl_type())),
false => None
}
}

/// Returns an Odra's entrypoint argument representation.
pub fn odra_argument<T: EntrypointArgument>(name: &str) -> Argument {
Argument {
ident: name.to_string(),
ty: T::cl_type(),
is_ref: false,
is_slice: false,
is_required: T::is_required()
}
}

#[cfg(test)]
mod tests {
use casper_types::U256;

use crate::{contract_context::MockContractContext, Address};

use super::*;

#[test]
fn test_maybe() {
let some = Maybe::Some(1);
let none: Maybe<u32> = Maybe::None;

let ctx = MockContractContext::new();
let env = ContractEnv::new(0, Rc::new(RefCell::new(ctx)));

assert!(some.is_some());
assert!(!some.is_none());
assert_eq!(some.clone().unwrap(&env), 1);
assert_eq!(some.unwrap_or_default(), 1);

assert!(!none.is_some());
assert!(none.is_none());
assert_eq!(none.unwrap_or_default(), 0);
}

#[test]
#[should_panic(expected = "revert")]
fn unwrap_on_none() {
let none: Maybe<u32> = Maybe::None;
let mut ctx = MockContractContext::new();
ctx.expect_revert().returning(|_| panic!("revert"));
let env = ContractEnv::new(0, Rc::new(RefCell::new(ctx)));

none.unwrap(&env);
}

#[test]
fn test_into_args() {
let args = [
odra_argument::<Maybe<u32>>("arg1"),
odra_argument::<U256>("arg2"),
odra_argument::<Option<String>>("arg3")
];

assert_eq!(args.len(), 3);
}

#[test]
fn test_into_casper_parameters() {
let params = [
parameter::<Maybe<u32>>("arg1"),
parameter::<Option<u32>>("arg2"),
parameter::<Maybe<Option<u32>>>("arg3"),
parameter::<Address>("arg4")
]
.into_iter()
.flatten()
.collect::<Vec<_>>();

assert_eq!(params.len(), 2);
}
}
16 changes: 6 additions & 10 deletions core/src/contract_container.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ mod tests {
#[test]
fn test_call_valid_entrypoint_with_wrong_arg_name() {
// Given an instance with a single entrypoint with one arg named "first".
let instance = ContractContainer::with_entrypoint(vec![("first", CLType::U32)]);
let instance = ContractContainer::with_entrypoint(vec!["first"]);

// When call the registered entrypoint with an arg named "second".
let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "second" => 0u32 });
Expand All @@ -118,7 +118,7 @@ mod tests {
#[test]
fn test_call_valid_entrypoint_with_wrong_arg_type() {
// Given an instance with a single entrypoint with one arg named "first".
let instance = ContractContainer::with_entrypoint(vec![("first", CLType::U32)]);
let instance = ContractContainer::with_entrypoint(vec!["first"]);

// When call the registered entrypoint with an arg named "second".
let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "first" => true });
Expand All @@ -137,7 +137,7 @@ mod tests {
#[test]
fn test_call_valid_entrypoint_with_missing_arg() {
// Given an instance with a single entrypoint with one arg named "first".
let instance = ContractContainer::with_entrypoint(vec![("first", CLType::U32)]);
let instance = ContractContainer::with_entrypoint(vec!["first"]);

// When call a valid entrypoint without args.
let call_def = CallDef::new(TEST_ENTRYPOINT, false, RuntimeArgs::new());
Expand All @@ -150,11 +150,7 @@ mod tests {
#[test]
fn test_many_missing_args() {
// Given an instance with a single entrypoint with "first", "second" and "third" args.
let instance = ContractContainer::with_entrypoint(vec![
("first", CLType::U32),
("second", CLType::U32),
("third", CLType::U32),
]);
let instance = ContractContainer::with_entrypoint(vec!["first", "second", "third"]);

// When call a valid entrypoint with a single valid args,
let call_def = CallDef::new(TEST_ENTRYPOINT, false, runtime_args! { "third" => 0u32 });
Expand All @@ -178,11 +174,11 @@ mod tests {
}
}

fn with_entrypoint(args: Vec<(&str, CLType)>) -> Self {
fn with_entrypoint(args: Vec<&str>) -> Self {
let entry_points = vec![EntryPoint::new(
String::from(TEST_ENTRYPOINT),
args.iter()
.map(|(name, ty)| Argument::new(String::from(*name), ty.to_owned()))
.map(|name| Argument::new::<u32>(String::from(*name)))
kpob marked this conversation as resolved.
Show resolved Hide resolved
.collect()
)];
let mut ctx = MockHostContext::new();
Expand Down
3 changes: 3 additions & 0 deletions core/src/contract_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,9 @@ pub trait ContractContext {
/// The value of the named argument as a byte array.
fn get_named_arg_bytes(&self, name: &str) -> Bytes;

/// Similar to `get_named_arg_bytes`, but returns `None` if the named argument is not present.
fn get_opt_named_arg_bytes(&self, name: &str) -> Option<Bytes>;

/// Handles the value attached to the call. Sets the value in the contract context.
fn handle_attached_value(&self);

Expand Down
7 changes: 5 additions & 2 deletions core/src/contract_def.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ pub struct Argument {
/// `true` if the argument is a reference.
pub is_ref: bool,
/// `true` if the argument is a slice.
pub is_slice: bool
pub is_slice: bool,
/// `true` if the argument is required.
pub is_required: bool
}

/// Defines an event.
Expand Down Expand Up @@ -168,7 +170,8 @@ impl<T: EventInstance> IntoEvent for T {
ident: name.clone(),
ty: ty.clone().downcast(),
is_ref: false,
is_slice: false
is_slice: false,
is_required: true
})
.collect::<Vec<_>>();
Event { ident, args }
Expand Down
13 changes: 10 additions & 3 deletions core/src/contract_env.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use crate::args::EntrypointArgument;
use crate::call_def::CallDef;
use crate::casper_types::bytesrepr::{Bytes, FromBytes, ToBytes};
use crate::casper_types::{CLTyped, U512};
Expand Down Expand Up @@ -236,9 +237,15 @@ impl ExecutionEnv {
///
/// The deserialized value of the named argument. If the argument does not exist or deserialization fails,
/// the contract will revert.
pub fn get_named_arg<T: FromBytes>(&self, name: &str) -> T {
let bytes = self.env.backend.borrow().get_named_arg_bytes(name);
deserialize_bytes(bytes, &self.env)
pub fn get_named_arg<T: FromBytes + EntrypointArgument>(&self, name: &str) -> T {
if T::is_required() {
let bytes = self.env.backend.borrow().get_named_arg_bytes(name);
deserialize_bytes(bytes, &self.env)
} else {
let bytes = self.env.backend.borrow().get_opt_named_arg_bytes(name);
let result = bytes.map(|bytes| deserialize_bytes(bytes, &self.env));
T::unwrap(result, &self.env)
}
}
}

Expand Down
8 changes: 6 additions & 2 deletions core/src/entry_point_callback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

use casper_types::CLType;

use crate::args::EntrypointArgument;
use crate::call_def::CallDef;
use crate::casper_types::bytesrepr::Bytes;
use crate::{host::HostEnv, prelude::*, ContractEnv, OdraResult};
Expand Down Expand Up @@ -85,7 +86,10 @@ pub struct Argument {

impl Argument {
/// Creates a new instance of `Argument`.
pub fn new(name: String, ty: CLType) -> Self {
Self { name, ty }
pub fn new<T: EntrypointArgument>(name: String) -> Self {
Self {
name,
ty: T::cl_type()
}
}
}
1 change: 1 addition & 0 deletions core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
extern crate alloc;

mod address;
pub mod args;
pub mod arithmetic;
mod call_def;
mod call_result;
Expand Down
3 changes: 3 additions & 0 deletions examples/Odra.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,6 @@ fqn = "features::pauseable::PauseableCounter"

[[contracts]]
fqn = "features::livenet::LivenetContract"

[[contracts]]
fqn = "features::optional_args::Token"
3 changes: 2 additions & 1 deletion examples/src/features/collecting_events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ mod test {
ident: "msg".to_string(),
ty: CLType::String,
is_ref: false,
is_slice: false
is_slice: false,
is_required: true
};
Event {
ident: "Info".to_string(),
Expand Down
Loading
Loading