diff --git a/docusaurus/docusaurus.config.js b/docusaurus/docusaurus.config.js index b44595f01..97be4c76d 100644 --- a/docusaurus/docusaurus.config.js +++ b/docusaurus/docusaurus.config.js @@ -54,7 +54,7 @@ const config = { sidebarPath: require.resolve('./sidebars.js'), includeCurrentVersion: true, showLastUpdateTime: true, - lastVersion: '1.1.0', + lastVersion: '1.2.0', versions: { current: { label: 'next', diff --git a/docusaurus/versioned_docs/version-1.2.0/advanced/01-delegate.md b/docusaurus/versioned_docs/version-1.2.0/advanced/01-delegate.md new file mode 100644 index 000000000..32b9b5f56 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/advanced/01-delegate.md @@ -0,0 +1,120 @@ +# Delegate + +Managing boilerplate code can often lead to code that is cumbersome and challenging to comprehend. The Odra library introduces a solution to this issue with its Delegate feature. As the name implies, the Delegate feature permits the delegation of function calls to child modules, effectively minimizing the redundancy of boilerplate code and maintaining a lean and orderly parent module. + +The main advantage of this feature is that it allows you to inherit the default behavior of child modules seamlessly, making your contracts more readable. + +## Overview + +To utilize the delegate feature in your contract, use the `delegate!` macro provided by Odra. This macro allows you to list the functions you wish to delegate to the child modules. By using the `delegate!` macro, your parent module remains clean and easy to understand. + +You can delegate functions to as many child modules as you like. The functions will be available as if they were implemented in the parent module itself. + +## Code Examples + +Consider the following basic example for better understanding: + +```rust +use crate::{erc20::Erc20, ownable::Ownable}; +use odra::{ + Address, casper_types::U256, + module::SubModule, + prelude::* +}; + +#[odra::module] +pub struct OwnedToken { + ownable: SubModule, + erc20: SubModule +} + +#[odra::module] +impl OwnedToken { + pub fn init(&mut self, name: String, symbol: String, decimals: u8, initial_supply: U256) { + let deployer = self.env().caller(); + self.ownable.init(deployer); + self.erc20.init(name, symbol, decimals, initial_supply); + } + + delegate! { + to self.erc20 { + fn transfer(&mut self, recipient: Address, amount: U256); + fn transfer_from(&mut self, owner: Address, recipient: Address, amount: U256); + fn approve(&mut self, spender: Address, amount: U256); + fn name(&self) -> String; + fn symbol(&self) -> String; + fn decimals(&self) -> u8; + fn total_supply(&self) -> U256; + fn balance_of(&self, owner: Address) -> U256; + fn allowance(&self, owner: Address, spender: Address) -> U256; + } + + to self.ownable { + fn get_owner(&self) -> Address; + fn change_ownership(&mut self, new_owner: Address); + } + } + + pub fn mint(&mut self, address: Address, amount: U256) { + self.ownable.ensure_ownership(self.env().caller()); + self.erc20.mint(address, amount); + } +} +``` + +This `OwnedToken` contract includes two modules: `Erc20` and `Ownable`. We delegate various functions from both modules using the `delegate!` macro. As a result, the contract retains its succinctness without compromising on functionality. + +The above example basically merges the functionalities of modules and adds some control over the minting process. But you can use delegation to build more complex contracts, cherry-picking just a few module functionalities. + +Let's take a look at another example. + +```rust +use crate::{erc20::Erc20, ownable::Ownable, exchange::Exchange}; +use odra::{ + Address, casper_types::U256, + module::SubModule, + prelude::* +}; + +#[odra::module] +pub struct DeFiPlatform { + ownable: SubModule, + erc20: SubModule, + exchange: SubModule +} + +#[odra::module] +impl DeFiPlatform { + pub fn init(&mut self, name: String, symbol: String, decimals: u8, initial_supply: U256, exchange_rate: u64) { + let deployer = self.env().caller(); + self.ownable.init(deployer); + self.erc20.init(name, symbol, decimals, initial_supply); + self.exchange.init(exchange_rate); + } + + delegate! { + to self.erc20 { + fn transfer(&mut self, recipient: Address, amount: U256); + fn balance_of(&self, owner: Address) -> U256; + } + + to self.ownable { + fn get_owner(&self) -> Address; + } + + to self.exchange { + fn swap(&mut self, sender: Address, recipient: Address); + fn set_exchange_rate(&mut self, new_rate: u64); + } + } + + pub fn mint(&mut self, address: Address, amount: U256) { + self.ownable.ensure_ownership(self.env().caller()); + self.erc20.mint(address, amount); + } +} +``` + +In this `DeFiPlatform` contract, we include `Erc20`, `Ownable`, and `Exchange` modules. By delegating functions from these modules, the parent contract becomes a powerhouse of functionality while retaining its readability and structure. + +Remember, the possibilities are endless with Odra's. By leveraging this feature, you can write cleaner, more efficient, and modular smart contracts. \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/advanced/02-advanced-storage.md b/docusaurus/versioned_docs/version-1.2.0/advanced/02-advanced-storage.md new file mode 100644 index 000000000..c14252873 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/advanced/02-advanced-storage.md @@ -0,0 +1,127 @@ +# Advanced Storage Concepts + +The Odra Framework provides advanced storage interaction capabilities that extend beyond the basic storage interaction. This document will focus on the `Mapping` and `Sequence` modules, which are key components of the advanced storage interaction in Odra. + +## Recap and Basic Concepts + +Before we delve into the advanced features, let's recap some basic storage concepts in Odra. In the realm of basic storage interaction, Odra provides several types for interacting with contract storage, including `Var`, `Mapping`, and `List`. These types enable contracts to store and retrieve data in a structured manner. The Var type is used to store a single value, while the List and Mapping types store collections of values. + +**Var**: A Var in Odra is a fundamental building block used for storing single values. Each Var is uniquely identified by its name in the contract. + +**Mapping**: Mapping in Odra serves as a key-value storage system. It stores an association of unique keys to values, and the value can be retrieved using the key. + +**List**: Built on top of the Var and Mapping building blocks, List in Odra allows storing an ordered collection of values that can be iterated over. + +If you need a refresher on these topics, please refer to our [guide](../basics/05-storage-interaction.md) on basic storage in Odra. + +## Advanced Storage Concepts + +### Sequence + +The `Sequence` in Odra is a basic module that stores a single value in the storage that can be read or incremented. Internally, holds a `Var` which keeps track of the current value. + +```rust +pub struct Sequence +where + T: Num + One + ToBytes + FromBytes + CLTyped +{ + value: Var +} +``` + +The Sequence module provides functions `get_current_value` and `next_value` to get the current value and increment the value respectively. + +### Advanced Mapping + +In Odra, a `Mapping` is a key-value storage system where the key is associated with a value. +In previous examples, the value of the `Mapping` typically comprised a standard serializable type (such as number, string, or bool) or a custom type marked with the `#[odra::odra_type]` attribute. + +However, there are more advanced scenarios where the value of the Mapping represents a module itself. This approach is beneficial when managing a collection of modules, each maintaining its unique state. + +Let's consider the following example: + +```rust title="examples/src/features/storage/mapping.rs" +use odra::{casper_types::U256, Mapping, UnwrapOrRevert}; +use odra::prelude::*; +use crate::owned_token::OwnedToken; + +#[odra::module] +pub struct Mappings { + strings: Mapping<(String, u32, String), String>, + tokens: Mapping +} + +#[odra::module] +impl Mappings { + + ... + + pub fn total_supply(&mut self, token_name: String) -> U256 { + self.tokens.module(&token_name).total_supply() + } + + pub fn get_string_api( + &self, + key1: String, + key2: u32, + key3: String + ) -> String { + let opt_string = self.strings.get(&(key1, key2, key3)); + opt_string.unwrap_or_revert(&self.env()) + } +} +``` + +As you can see, a `Mapping` key can consist of a tuple of values, not limited to a single value. + +:::note +Accessing Odra modules differs from accessing regular values such as strings or numbers. + +Firstly, within a `Mapping`, you don't encapsulate the module with `Submodule`. + +Secondly, rather than utilizing the `Mapping::get()` function, call `Mapping::module()`, which returns `SubModule` and sets the appropriate namespace for nested modules. +::: + +## AdvancedStorage Contract + +The given code snippet showcases the `AdvancedStorage` contract that incorporates these storage concepts. + +```rust +use odra::{Address, casper_types::U512, Sequence, Mapping}; +use odra::prelude::*; +use crate::modules::Token; + +#[odra::module] +pub struct AdvancedStorage { + counter: Sequence, + tokens: Mapping<(String, String), Token>, +} + +impl AdvancedStorage { + pub fn current_value(&self) -> u32 { + self.counter.get_current_value() + } + + pub fn increment_and_get(&mut self) -> u32 { + self.counter.next_value() + } + + pub fn balance_of(&mut self, token_name: String, creator: String, address: Address) -> U512 { + let token = self.tokens.module(&(token_name, creator)); + token.balance_of(&address) + } + + pub fn mint(&self, token_name: String, creator: String, amount: U512, to: Address) { + let mut token = self.tokens.module(&(token_name, creator)); + token.mint(amount, to); + } +} +``` + +## Conclusion + +Advanced storage features in Odra offer robust options for managing contract state. Two key takeaways from this document are: +1. Odra offers a Sequence module, enabling contracts to store and increment a single value. +2. Mappings support composite keys expressed as tuples and can store modules as values. + +Understanding these concepts can help developers design and implement more efficient and flexible smart contracts. diff --git a/docusaurus/versioned_docs/version-1.2.0/advanced/03-attributes.md b/docusaurus/versioned_docs/version-1.2.0/advanced/03-attributes.md new file mode 100644 index 000000000..40c697afe --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/advanced/03-attributes.md @@ -0,0 +1,117 @@ +# Attributes + +Smart contract developers with Ethereum background are familiar with Solidity's concept of modifiers in Solidity - a feature that +allows developers to embed common checks into function definitions in a readable and reusable manner. +These are essentially prerequisites for function execution. + +Odra defines a few attributes that can be applied to functions to equip them with superpowers. + +## Payable + +When writing a smart contract, you need to make sure that money can be both sent to and extracted from the contract. The 'payable' attribute helps wit this. Any function, except for a constructor, with the `#[odra(payable)]` attribute can send and take money in the form of native tokens. + +### Example + +```rust title=examples/src/contracts/tlw.rs +#[odra(payable)] +pub fn deposit(&mut self) { + // Extract values + let caller: Address = self.env().caller(); + let amount: U256 = self.env().attached_value(); + let current_block_time: u64 = self.env().get_block_time(); + + // Multiple lock check + if self.balances.get(&caller).is_some() { + self.env.revert(Error::CannotLockTwice) + } + + // Update state, emit event + self.balances.set(&caller, amount); + self.lock_expiration_map + .set(&caller, current_block_time + self.lock_duration()); + self.env() + .emit_event(Deposit { + address: caller, + amount + }); +} +``` + +If you try to send tokens to a non-payable function, the transaction will be automatically rejected. + + +## Non Reentrant + +Reentrancy attacks in smart contracts exploit the possibility of a function being called multiple times before its initial execution is completed, leading to the repeated unauthorized withdrawal of funds. + +To prevent such attacks, developers should ensure that all effects on the contract's state and balance checks occur before calling external contracts. + +They can also use reentrancy guards to block recursive calls to sensitive functions. + +In Odra you can just apply the `#[odra(non_reentrant)]` attribute to your function. + +### Example + +```rust +#[odra::module] +pub struct NonReentrantCounter { + counter: Var +} + +#[odra::module] +impl NonReentrantCounter { + #[odra(non_reentrant)] + pub fn count_ref_recursive(&mut self, n: u32) { + if n > 0 { + self.count(); + ReentrancyMockRef::new(self.env(), self.env().self_address()).count_ref_recursive(n - 1); + } + } +} + +impl NonReentrantCounter { + fn count(&mut self) { + let c = self.counter.get_or_default(); + self.counter.set(c + 1); + } +} + +#[cfg(test)] +mod test { + use super::*; + use odra::{host::{Deployer, NoArgs}, ExecutionError}; + + #[test] + fn ref_recursion_not_allowed() { + let test_env = odra_test::env(); + let mut contract = NonReentrantCounterHostRef::deploy(&test_env, NoArgs); + + let result = contract.count_ref_recursive(11); + assert_eq!(result, ExecutionError::ReentrantCall.into()); + } +} +``` + +## Mixing attributes + +A function can accept more than one attribute. The only exclusion is a constructor cannot be payable. +To apply multiple attributes, you can write: + +```rust +#[odra(payable, non_reentrant)] +fn deposit() { + // your logic... +} +``` + +or + +```rust +#[odra(payable)] +#[odra(non_reentrant)] +fn deposit() { + // your logic... +} +``` + +In both cases attributes order does not matter. diff --git a/docusaurus/versioned_docs/version-1.2.0/advanced/04-storage-layout.md b/docusaurus/versioned_docs/version-1.2.0/advanced/04-storage-layout.md new file mode 100644 index 000000000..d819c46a5 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/advanced/04-storage-layout.md @@ -0,0 +1,100 @@ +# Storage Layout + +Odra's innovative modular design necessitates a unique storage layout. This +article explains step-by-step Odra's storage layout. + +## Casper VM Perspective +The Casper Execution Engine (VM) enables the storage of data in named keys or +dictionaries. However, a smart contract has a limited number of named keys, +making it unsuitable for storing substantial data volumes. Odra resolves this +issue by storing all user-generated data in a dictionary called `state`. This +dictionary operates as a key-value store, where keys are strings with a maximum +length of 64 characters, and values are arbitrary byte arrays. + +Here is an example of what the interface for reading and writing data could look +like: + +```rust +pub trait CasperStorage { + fn read(key: &str) -> Option>; + fn write(key: &str, value: Vec); +} +``` + +## Odra Perspective +Odra was conceived with modularity and code reusability in mind. Additionally, +we aimed to streamline storage definition through the struct object. Consider +this straightforward storage definition: + +```rust +#[odra::module] +pub struct Token { + name: Var, + balances: Mapping +} +``` + +The `Token` structure contains two fields: `name` of type `String` and +`balances`, which functions as a key-value store with `Address` as keys and +`U256` as values. + +The `Token` module can be reused in another module, as demonstrated in a more +complex example: + +```rust +#[odra::module] +pub struct Loans { + lenders: SubModule, + borrowers: SubModule, +} +``` + +The `Loans` module has two fields: `lenders` and `borrowers`, both of which have +the same storage layout as defined by the `Token` module. Odra guarantees that +`lenders` and `borrowers` are stored under distinct keys within the storage +dictionary. + +Both `Token` and `Loans` serve as examples to show how Odra's storage layout +operates. + +## Key generation. + +Every element of a module (`struct`) with N elements is associated with an index +ranging from 0 to N-1, represented as a u8 with a maximum of 256 elements. If an +element of a module is another module (`SubModule<...>`), the associated index +serves as a prefix for the indexes of the inner module. + +While this may initially appear complex, it is easily understood through an +example. In the example, indexes are presented as bytes, reflecting the actual +implementation. + +``` +Loans { + lenders: Token { // prefix: 0x0001 + name: 1, // key: 0x0001_0001 + balances: 2 // key: 0x0001_0010 + }, + borrowers: Token { // prefix: 0x0010 + name: 1, // key: 0x0010_0001 + balances: 2 // key: 0x0010_0010 + } +} +``` + +Additionally, it's worth mentioning how `Mapping`'s keys are used in the +`storage`. They are simply concatenated with the index of the module, as +demonstrated in the example. + +For instance, triggering `borrowers.balances.get(0x1234abcd)` would result in a +key: +``` +0x0001_0001_1234_abcd +``` + +Finally, the key must be hashed to fit within the 64-character limit and then +encoded in hexadecimal format. + +## Value serialization +Before being stored in the storage, each value is serialized into bytes using +the `CLType` serialization method and subsequently encapsulated with Casper's +`Bytes` types. diff --git a/docusaurus/versioned_docs/version-1.2.0/advanced/05-using-different-allocator.md b/docusaurus/versioned_docs/version-1.2.0/advanced/05-using-different-allocator.md new file mode 100644 index 000000000..ee0ea507e --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/advanced/05-using-different-allocator.md @@ -0,0 +1,24 @@ +# Memory allocators + +When compiling contracts to wasm, your code needs to be `no-std`. +This means that instead of using the standard library, the `core` +crate will be linked to your code. This crate does not contain +a memory allocator. + +Happily, Odra automatically enables allocator - from our tests +the one developed by [ink!](https://docs.rs/ink_allocator/latest/ink_allocator/) +seems to be the best. + +## Using a different allocator + +If the default allocator does not suit your needs, or you use a crate that +already provides an allocator, you can disable the default allocator by enabling +the `disable-allocator` feature in the `odra` dependency in your project: + +```toml +[dependencies] +odra = { path = "../odra", features = ["disable-allocator"] } +``` + +If you want to have a better control over the features that are enabled +during the building and tests, see the next article on [building manually](06-building-manually.md). \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/advanced/06-building-manually.md b/docusaurus/versioned_docs/version-1.2.0/advanced/06-building-manually.md new file mode 100644 index 000000000..781e291cd --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/advanced/06-building-manually.md @@ -0,0 +1,77 @@ +# Building contracts manually + +`cargo odra` is a great tool to build and test your contracts, but sometimes + a better control over the parameters that are passed to the `cargo` +or the compiler is needed. + +This is especially useful when the project has multiple features, and there is a need +to switch between them during the building and testing. + +Knowing that `cargo odra` is a simple wrapper around `cargo`, it is easy to replicate +the same behavior by using `cargo` directly. + +## Building the contract manually + +To build the contract manually, `cargo odra` uses the following command: + +```bash +ODRA_MODULE=my_contract cargo build --release --target wasm32-unknown-unknown --bin my_project_build_contract +``` + +:::info +Odra uses the environment variable `ODRA_MODULE` to determine which contract to build. +::: + +Assuming that project's crate is named `my_project`, this command will build +the `my_contract` contract in release mode and generate the wasm file. +The file will be put into the `target/wasm32-unknown-unknown/release` directory under +the name `my_project_build_contract.wasm`. + +The Odra Framework expects the contracts to be placed in the `wasm` directory, and +to be named correctly, so the next step would be to move the file: + +```bash +mv target/wasm32-unknown-unknown/release/my_project_build_contract.wasm wasm/my_contract.wasm +``` + +## Optimizing the contract + +To lower the size of the wasm file, `cargo odra` uses the `wasm-strip` tool: + +```bash +wasm-strip wasm/my_contract.wasm +``` + +To further optimize the wasm file, the `wasm-opt` tool is also used. +```bash +wasm-opt --signext-lowering wasm/my_contract.wasm -o wasm/my_contract.wasm +``` + +:::warning +This step is required, as the wasm file generated by the Rust compiler is not +fully compatible with the Casper execution engine. +::: + +## Running the tests manually + +To run the tests manually, Odra needs to know which backend to use. +To run tests against Casper backend, the following command needs to be used: + +```bash +ODRA_BACKEND=casper cargo test +``` + +## Wrapping up + +Let's say we want to build the `my_contract` in debug mode, run the tests against the +casper backend and use the `my-own-allocator` feature from our `my_project` project. + +To do that, we can use the following set of commands: + +```bash +ODRA_MODULE=my_contract cargo build --target wasm32-unknown-unknown --bin my_project_build_contract +mv target/wasm32-unknown-unknown/debug/my_project_build_contract.wasm wasm/my_contract.wasm +wasm-strip wasm/my_contract.wasm +wasm-opt --signext-lowering wasm/my_contract.wasm -o wasm/my_contract.wasm +ODRA_BACKEND=casper cargo test --features my-own-allocator +``` diff --git a/docusaurus/versioned_docs/version-1.2.0/advanced/07-signatures.md b/docusaurus/versioned_docs/version-1.2.0/advanced/07-signatures.md new file mode 100644 index 000000000..8db940768 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/advanced/07-signatures.md @@ -0,0 +1,90 @@ +# Signatures + +Odra Framework provides a function for easy signature verification inside the contract context and +corresponding functions in the `HostEnv` for testing purposes. + +## Signature verification + +Signature verification is conducted by a function in contract's `env()`: + +```rust +pub fn verify_signature(message: &Bytes, signature: &Bytes, public_key: &PublicKey) -> bool; +``` + +Here's the simplest example of this function used in a contract: + +```rust title=examples/src/features/signature_verifier.rs +#[odra::module] +impl SignatureVerifier { + pub fn verify_signature( + &self, + message: &Bytes, + signature: &Bytes, + public_key: &PublicKey + ) -> bool { + self.env().verify_signature(message, signature, public_key) + } +} +``` + +## Testing +Besides the above function in the contract context, Odra provides corresponding functions in the `HostEnv`: + +```rust +pub fn sign_message(message: &Bytes, address: &Address) -> Bytes; + +pub fn public_key(address: &Address) -> PublicKey; +``` + +`sign_message` will return a signed message. The signing itself will be performed using a private key +of an account behind the `address`. + +`public_key` returns the PublicKey of an `address` account. + +Here's a complete example of how to test the signature verification +in the contract: +```rust title=examples/src/features/signature_verifier.rs +#[test] +fn signature_verification_works() { + let test_env = odra_test::env(); + let message = "Message to be signed"; + let message_bytes = Bytes::from(message.as_bytes()); + let account = test_env.get_account(0); + + let signature = test_env.sign_message(&message_bytes, &account); + + let public_key = test_env.public_key(&account); + + let signature_verifier = SignatureVerifierHostRef::deploy(&test_env, NoArgs); + assert!(signature_verifier.verify_signature(&message_bytes, &signature, &public_key)); +} +``` + +If you want, you can also test signatures created outside Odra: + +```rust title=examples/src/features/signature_verifier.rs +#[test] +fn verify_signature_casper_wallet() { + // Casper Wallet for the message "Ahoj przygodo!" signed using SECP256K1 key + // produces the following signature: + // 1e87e186238fa1df9c222b387a79910388c6ef56285924c7e4f6d7e77ed1d6c61815312cf66a5318db204c693b79e020b1d392dafe8c1b3841e1f6b4c41ca0fa + // Casper Wallet adds "Casper Message:\n" prefix to the message: + let message = "Casper Message:\nAhoj przygodo!"; + let message_bytes = Bytes::from(message.as_bytes()); + + // Depending on the type of the key, we need to prefix the signature with a tag: + // 0x01 for ED25519 + // 0x02 for SECP256K1 + let signature_hex = "021e87e186238fa1df9c222b387a79910388c6ef56285924c7e4f6d7e77ed1d6c61815312cf66a5318db204c693b79e020b1d392dafe8c1b3841e1f6b4c41ca0fa"; + let signature: [u8; 65] = hex::decode(signature_hex).unwrap().try_into().unwrap(); + let signature_bytes = Bytes::from(signature.as_slice()); + + // Similar to the above, the public key is tagged: + let public_key_hex = "02036d9b880e44254afaf34330e57703a63aec53b5918d4470059b67a4a906350105"; + let public_key_decoded = hex::decode(public_key_hex).unwrap(); + let (public_key, _) = PublicKey::from_bytes(public_key_decoded.as_slice()).unwrap(); + + let signature_verifier = SignatureVerifierHostRef::deploy(&odra_test::env(), NoArgs); + assert!(signature_verifier.verify_signature(&message_bytes, &signature_bytes, &public_key)); +} +``` \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/advanced/_category_.json b/docusaurus/versioned_docs/version-1.2.0/advanced/_category_.json new file mode 100644 index 000000000..2fc5773cb --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/advanced/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Advanced", + "position": 3, + "link": { + "type": "generated-index", + "description": "Advanced concepts of Odra Framework" + } +} diff --git a/docusaurus/versioned_docs/version-1.2.0/backends/01-what-is-a-backend.md b/docusaurus/versioned_docs/version-1.2.0/backends/01-what-is-a-backend.md new file mode 100644 index 000000000..b815aa88a --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/backends/01-what-is-a-backend.md @@ -0,0 +1,35 @@ +--- +sidebar_position: 1 +--- + +# What is a backend? + +You can think of a backend as a target platform for your smart contract. +This can be a piece of code allowing you to quickly check your code - like [OdraVM](02-odra-vm.md), +a complete virtual machine, spinning up a blockchain for you - like [CasperVM](03-casper.md), +or even a real blockchain - when using [Livenet backend](03-casper.md). + +Each backend has to come with two parts that Odra requires - the Contract Env and the Host Env. + +## Contract Env +The Contract Env is a simple interface that each backend needs to implement, +exposing features of the blockchain from the perspective of the contract. + +It gives Odra a set of functions, which allows implementing more complex concepts - +for example, to implement [Mapping](../basics/05-storage-interaction.md), +Odra requires some kind of storage integration. +The exact implementation of those functions is a responsibility of a backend, +making Odra and its user free to implement the contract logic, +instead of messing with the blockchain internals. + +Other functions from Contract Env include handling transfers, addresses, block time, errors and events. + +## Host Env +Similarly to the Contract Env, the Host Env exposes a set of functions that allows the communication with +the backend from the outside world - really useful for implementing tests. + +This ranges from interacting with the blockchain - like deploying new, loading existing and calling the contracts, +to the more test-oriented - handling errors, forwarding the block time, etc. + +## What's next +We will take a look at backends Odra implements in more detail. \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/backends/02-odra-vm.md b/docusaurus/versioned_docs/version-1.2.0/backends/02-odra-vm.md new file mode 100644 index 000000000..653478489 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/backends/02-odra-vm.md @@ -0,0 +1,47 @@ +--- +sidebar_position: 2 +--- + +# OdraVM + +The OdraVM is a simple implementation of a mock backend with a minimal set of features that allows testing +the code written in Odra without compiling the contract to the target architecture and spinning up the +blockchain. + +Thanks to OdraVM tests run a lot faster than other backends. You can even debug the code in real time - +simply use your IDE's debug functionality. + +## Usage +The OdraVM is the default backend for Odra framework, so each time you run + +```bash +cargo odra test +``` + +You are running your code against it. + +## Architecture +OdraVM consists of two main parts: the `Contract Register` and the `State`. + +The `Contract Register` is a list of contracts deployed onto the OdraVM, identified by an `Address`. + +Contracts and Test Env functions can modify the `State` of the OdraVM. + +Contrary to the "real" backend, which holds the whole history of the blockchain, +the OdraVM `State` holds only the current state of the OdraVM. +Thanks to this and the fact that we do not need the blockchain itself, +OdraVM starts instantly and runs the tests in the native speed. + +## Execution + +When the OdraVM backend is enabled, the `#[odra::module]` attribute is responsible for converting +your `pub` functions into a list of Entrypoints, which are put into a `Contract Container`. +When the contract is deployed, its Container registered into a Registry under an address. +During the contract call, OdraVM finds an Entrypoint and executes the code. + +```mermaid +graph TD; + id1[[Odra code]]-->id2[Contract Container]; + id2[Contract Container]-->id3((Contract Registry)) + id3((Contract Registry))-->id4[(OdraVM Execution)] +``` \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/backends/03-casper.md b/docusaurus/versioned_docs/version-1.2.0/backends/03-casper.md new file mode 100644 index 000000000..7a58e854e --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/backends/03-casper.md @@ -0,0 +1,257 @@ +--- +sidebar_position: 3 +--- + +# Casper +The Casper backend allows you to compile your contracts into WASM files which can be deployed +onto [Casper Blockchain](https://casper.network/) +and lets you to easily run them against [Casper's Execution Engine][casper_engine] locally. + +## Contract Env + +As with any other backend, Casper Backend must implement the same features, but some do not have native support. Let's take a closer look at how Odra overcomes these hindrances. + +### Events +An event is not a first-class citizen in Casper like in Ethereum, so Odra mimics it. As you've +already learned from the [events article](../basics/09-events.md), in Odra you emit an event, similarly, you would do it in [Solidity][events_sol]. + +Under the hood, Odra integrates with [Casper Event Standard] and creates a few [`URef`s][uref] in the global state when a contract is being installed: +1. `__events` - a dictionary that stores events' data. +2. `__events_length` - the evens count. +3. `__events_ces_version` - the version of `Casper Event Standard`. +4. `__events_schema` - a dictionary that stores event schemas. + +Besides that, all the events the contract emits are registered - events schemas are written to the storage under the `__events_schema` key. + +So, `Events` are nothing different from any other data stored by a contract. + +A struct to be an event must implement traits defined by [Casper Event Standard], thankfully you can derive them using `#[odra::event]`. + +:::note +Don't forget to expose events in the module using `#[odra::module(events = [...])]`. +::: + +### Payable +The first Odra idiom is a `Contract Main Purse`. It is a purse associated with a contract. The purse is created lazily - when the first transfer to the contract occurs, a proper `URef` and a purse are created and stored under the `__contract_main_purse` key. + +Casper does not allow direct transfers from an account to a contract, so Odra comes up with the second idiom - a `Cargo Purse`. It is a one-time-use purse proxy between an account and a contract. First, motes go from the account to the cargo purse and then to the contract's main purse. + +Behind the scenes, Odra handles an account-contract transfer via a cargo purse when a function is marked as payable. +If under the way something goes wrong with the transfer, the contract reverts. + +The transferred amount can be read inside the contract by calling `self.env().attached_value()`. + +:::note +Odra expects the `cargo_purse` runtime argument to be attached to a contract call. +In case of its absence, the `contract_env::attached_value()` returns zero. +::: + +### Revert +In Casper, we can stop the execution pretty straightforwardly - call the `runtime::revert()`. +Odra adds an extra abstraction layer - in a contract `ExecutionError`s are defined, which ultimately are transformed into Casper's [`ApiError::User`][api_error]. + +### Context +Casper equips developers with very low-level tooling, which can be cumbersome for newcomers. +If you want to check who called the contract or its address, you can not do it off-hand - you must analyze the call stack. + +The `self.env().self_address()` function takes the first element of the callstack ([`runtime::get_call_stack()`][callstack]) and casts it to `Address`. + +The `self.env().caller()` function takes the second element of the call stack (`runtime::get_call_stack()`) and casts it to `Address`. + +As mentioned in the [Payable] section, to store CSPR, each contract creates its purse. To read the contract balance, +you call `self.env().self_balance()`, which checks the balance of the purse stored under `__contract_main_purse`. + +## Test Env +Test environment allows you to test wasm contracts before you deploy them onto the testnet or livenet. It is built on top of the `Casper Execution Engine`. + +In your test, you can freely switch execution context by setting as a caller (`test_env::set_caller()`) one of the 20 predefined accounts. Each account possesses the default amount of `Motes` (100_000_000_000_000_000). + +The Test Env internally keeps track of the current `block time`, `error` and `attached value`. + +Each test is executed on a fresh instance of the Test Env. + +## Usage +Name of the Casper backend in Odra is `casper`, so to run the tests against it, simply pass it as a `-b` +parameter: + +```bash +cargo odra test -b casper +``` + +If you want to just generate a wasm file, simply run: + +```bash +cargo odra build -b casper +``` + +## Deploying a contract to Casper network + +There would be no point in writing a contract if you couldn't deploy it to the blockchain. +You can do it in two ways: provided by the Casper itself: using the `casper-client` tool +or using the Odra's Livenet integration. + +Let's explore the first option to better understand the process. + +:::note +If you wish, you can skip the following section and jump to the [Livenet integration](04-livenet.md). +::: + +### WASM arguments + +When deploying a new contract you can pass some arguments to it. +Every contract written in Odra expects those arguments to be set: + +- `odra_cfg_package_hash_key_name` - `String` type. The key under which the package hash of the contract will be stored. +- `odra_cfg_allow_key_override` - `Bool` type. If `true` and the key specified in `odra_cfg_package_hash_key_name` already exists, it will be overwritten. +- `odra_cfg_is_upgradable` - `Bool` type. If `true`, the contract will be deployed as upgradable. + +Additionally, if required by the contract, you can pass constructor arguments. + +When working with the test env via `cargo odra` or when using +[Livenet integration](04-livenet.md) this is handled automatically. However, if you rather use +`casper-client` directly, you have to pass them manually: + +### Example: Deploy Counter + +To deploy your contract with a constructor using `casper-client`, you need to pass the above arguments. +Additionally, you need to pass the `value` argument, which sets the arbitrary initial value for the counter. + +```bash +casper-client put-deploy \ + --node-address [NODE_ADDRESS] \ + --chain-name casper-test \ + --secret-key [PATH_TO_YOUR_KEY]/secret_key.pem \ + --payment-amount 5000000000000 \ + --session-path ./wasm/counter.wasm \ + --session-arg "odra_cfg_package_hash_key_name:string:'counter_package_hash'" \ + --session-arg "odra_cfg_allow_key_override:bool:'true'" \ + --session-arg "odra_cfg_is_upgradable:bool:'true'" \ + --session-arg "value:u32:42" +``` + +For a more in-depth tutorial, please refer to the [Casper's 'Writing On-Chain Code']. + +### Example: Deploy ERC721 + +Odra comes with a standard ERC721 token implementation. +Clone the main Odra repo and navigate to the `modules` directory. + +Firstly contract needs to be compiled. +```bash +cargo odra build -b casper -c erc721_token +``` + +It produces the `erc721_token.wasm` file in the `wasm` directory. + +Now it's time to deploy the contract. +```bash +casper-client put-deploy \ + --node-address [NODE_ADDRESS] \ + --chain-name casper-test \ + --secret-key [PATH_TO_YOUR_KEY]/secret_key.pem \ + --payment-amount 300000000000 \ + --session-path ./wasm/erc721_token.wasm \ + --session-arg "odra_cfg_package_hash_key_name:string:'my_nft'" \ + --session-arg "odra_cfg_allow_key_override:bool:'false'" \ + --session-arg "odra_cfg_is_upgradable:bool:'true'" \ + --session-arg "name:string:'MyNFT'" \ + --session-arg "symbol:string:'NFT'" \ + --session-arg "base_uri:string:'https://example.com/'" +``` + +It's done. +The contract is deployed and ready to use. +Your account is the owner of the contract and you can mint and burn tokens. +For more details see the code of the [ERC721] module. + +To obtain the package hash of the contract search for `my_nft` key +in your account's named keys. + +### Example: Deploy ERC1155 + +The process is similar to the one described in the previous section. + +Contract compilation: +```bash +cargo odra build -b casper -c erc1155_token +``` + +Contract deployment: +```bash +casper-client put-deploy \ + --node-address [NODE_ADDRESS] \ + --chain-name casper-test \ + --secret-key [PATH_TO_YOUR_KEY]/secret_key.pem \ + --payment-amount 300000000000 \ + --session-path ./wasm/erc1155_token.wasm \ + --session-arg "odra_cfg_package_hash_key_name:string:'my_tokens'" \ + --session-arg "odra_cfg_allow_key_override:bool:'false'" \ + --session-arg "odra_cfg_is_upgradable:bool:'true'" \ + --session-arg "odra_cfg_constructor:string:'init'" \ +``` + +As previously, your account is the owner and can mint and burn tokens. +For more details see the code of the [ERC1155] module. + +## Sending CSPR to a contract + +Defining payable entry points is described in [Native Token](../basics/12-native-token.md) section. + +What is happening under the hood is that Odra creates a new `cargo_purse` argument for each payable +entry point. The `cargo_purse` needs to be top-upped with CSPR before calling the contract. +When a contract adds CSPR to another contract call, Odra handles it for you. +The problem arises when you want to call an entry point and attach CSPR as an account. +The only way of doing that is by executing code in the sessions context, that +top-ups the `cargo_purse` and then calls the contract. + +Odra provides a generic `proxy_caller.wasm` that does exactly that. +You can build it by yourself from the main Odra repository, or use the [proxy_caller.wasm] +we maintain. + +### Using proxy_caller.wasm + +To use the `proxy_caller.wasm` you need to attach the following arguments: + +- `contract_package_hash` - `BytesArray(32)` type. The package hash of the contract you want to call. +Result of `to_bytes` on [CasperPackageHash]. +- `entry_point` - `String` type. The name of the entry point you want to call. +- `args` - `Bytes` type. It is a serialized [RuntimeArgs] with the arguments you want to pass +to the entry point. To be specific it is the result of `to_bytes` method wrapped with [Bytes] type. +- `attached_value`. `U512` type. The amount of CSPR you want to attach to the call. +- `amount`. `U512` type. Should be the same value as `attached_value` if not `None`. +It is a special Casper argument that enables the access to account's main purse. + +Currently `casper-client` doesn't allow building such arguments. +You have to build it using your SDK. See an example in the [Tutorial section]. + +## Execution + +First thing Odra does with your code, is similar to the one used in [OdraVM](02-odra-vm.md) - +a list of entrypoints is generated, thanks to the `#[odra::module]` attribute. + +```mermaid +graph TD; + id1[[Odra code]]-->id2[IR]; + id2[IR]-->id3((WASM)) + id3((WASM))-->id4[(Local Casper\nExecution Engine)] + id3((WASM))-->id5[(Casper Network)] +``` + +[casper_engine]: https://crates.io/crates/casper-execution-engine +[events_sol]: https://docs.soliditylang.org/en/v0.8.15/contracts.html#example +[uref]: https://docs.rs/casper-types/latest/casper_types/struct.URef.html +[callstack]: https://docs.rs/casper-contract/latest/casper_contract/contract_api/runtime/fn.get_call_stack.html +[runtime_args]: https://docs.rs/casper-types/latest/casper_types/runtime_args/struct.RuntimeArgs.html +[account_hash]: https://docs.rs/casper-types/latest/casper_types/account/struct.AccountHash.html +[contract_package_hash]: https://docs.rs/casper-types/latest/casper_types/struct.ContractPackageHash.html +[api_error]: https://docs.rs/casper-types/latest/casper_types/enum.ApiError.html +[deploy]: https://docs.rs/casper-execution-engine/latest/casper_execution_engine/core/engine_state/deploy_item/struct.DeployItem.html +[Casper Event Standard]: https://github.com/make-software/casper-event-standard +[Casper's 'Writing On-Chain Code']: https://docs.casper.network/writing-contracts/ +[proxy_caller.wasm]: https://github.com/odradev/odra/blob/release/1.1.0/odra-casper/test-vm/resources/proxy_caller.wasm +[CasperPackageHash]: https://docs.rs/casper-types/latest/casper_types/contracts/struct.ContractPackageHash.html +[RuntimeArgs]: https://docs.rs/casper-types/latest/casper_types/runtime_args/struct.RuntimeArgs.html +[Bytes]: https://docs.rs/casper-types/latest/casper_types/bytesrepr/struct.Bytes.html +[ERC721]: https://github.com/odradev/odra/blob/release/1.1.0/modules/src/erc721_token.rs +[ERC1155]: https://github.com/odradev/odra/blob/release/1.1.0/modules/src/erc1155_token.rs +[Tutorial section]: ../tutorials/using-proxy-caller \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/backends/04-livenet.md b/docusaurus/versioned_docs/version-1.2.0/backends/04-livenet.md new file mode 100644 index 000000000..9592ad2ac --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/backends/04-livenet.md @@ -0,0 +1,204 @@ +--- +sidebar_position: 3 +--- + +# Livenet + +The Livenet backend let us deploy and test the contracts on the real blockchain. It can be a local +test node, a testnet or even the mainnet. It is possible and even recommended using the Livenet backend +to handle the deployment of your contracts to the real blockchain. + +Furthermore, it is implemented in a similarly to Casper or OdraVM, +however, it uses a real blockchain to deploy contracts and store the state. +This lets us use Odra to deploy and test contracts on a real blockchain, but +on the other hand, it comes with some limitations on what can be done in the tests. + +The main differences between Livenet and e.g. CasperVM backend are: +- Real CSPR tokens are used to deploy and call contracts. This also means that we need to +pay for each contract deployment and each contract call. Of course, we can use the [faucet](https://testnet.cspr.live/tools/faucet) +to get some tokens for testing purposes, but we still need to specify the amount needed +for each action. +- The contract state is stored on the real blockchain, so we can't just reset the state - +we can redeploy the contract, but we can't remove the old one. +- Because of the above, we can load the existing contracts and use them in the tests. +- We have no control over the block time. This means that for example, `advance_block_time` function +is implemented by waiting for the real time to pass. + +This is also a cause for the fact that the Livenet backend cannot be (yet) used for running +the regular Odra tests. Instead, we can create integration tests or binaries which will +use a slightly different workflow to test the contracts. + +## Setup + +To use Livenet backend, we need to provide Odra with some information - the network address, our private +key and the name of the chain we want to use. Optionally, we can add multiple private keys to use +more than one account in our tests. Those values are passed using environment variables. We can use .env +file to store them - let's take a look at an example .env file, created from the [.env.sample] file from +examples folder: + +```env +# Path to the secret key of the account that will be used +# to deploy the contracts. +# We're using .keys folder so we don't accidentally commit +# the secret key to the repository. +ODRA_CASPER_LIVENET_SECRET_KEY_PATH=.keys/secret_key.pem + +# RPC address of the node that will be used to deploy the contracts. +ODRA_CASPER_LIVENET_NODE_ADDRESS=localhost:7777 + +# Chain name of the network. Known values: +# - integration-test +ODRA_CASPER_LIVENET_CHAIN_NAME=integration-test + +# Paths to the secret keys of the additional accounts. +# Main secret key will be 0th account. +ODRA_CASPER_LIVENET_KEY_1=.keys/secret_key_1.pem +ODRA_CASPER_LIVENET_KEY_2=.keys/secret_key_2.pem + +# If using CSPR.cloud, you can set the auth token here. +# CSPR_CLOUD_AUTH_TOKEN= +``` + +:::note +CSPR.cloud is a service that provides mainnet and testnet Casper nodes on demand. +::: + +With the proper value in place, we can write our tests or deploy scenarios. In the examples, we can find +a simple binary that deploys a contract and calls it. The test is located in the [erc20_on_livenet.rs] file. +Let's go through the code: + +```rust +fn main() { + // Similar to the OdraVM backend, we need to initialize + // the environment: + let env = odra_casper_livenet_env::env(); + + // Most of the for the host env works the same as in the + // OdraVM backend. + let owner = env.caller(); + // Addresses are the real addresses on the blockchain, + // so we need to provide them + // if we did not import their secret keys. + let recipient = + "hash-2c4a6ce0da5d175e9638ec0830e01dd6cf5f4b1fbb0724f7d2d9de12b1e0f840"; + let recipient = Address::from_str(recipient).unwrap(); + + // Arguments for the contract init method. + let name = String::from("Plascoin"); + let symbol = String::from("PLS"); + let decimals = 10u8; + let initial_supply: U256 = U256::from(10_000); + + // The main difference between other backends - we need to specify + // the gas limit for each action. + // The limit will be used for every consecutive action + // until we change it. + env.set_gas(100_000_000_000u64); + + // Deploy the contract. The API is the same as in the OdraVM backend. + let init_args = Erc20InitArgs { + name, + symbol, + decimals, + initial_supply: Some(initial_supply) + }; + let mut token = Erc20HostRef::deploy(env, init_args); + + // We can now use the contract as we would in the OdraVM backend. + println!("Token address: {}", token.address().to_string()); + + // Uncomment to load existing contract. + // let address = "hash-d26fcbd2106e37be975d2045c580334a6d7b9d0a241c2358a4db970dfd516945"; + // let address = Address::from_str(address).unwrap(); + // We use the Livenet-specific `load` method to load the contract + // that is already deployed. + // let mut token = Erc20Deployer::load(env, address); + + // Non-mutable calls are free! Neat, huh? More on that later. + println!("Token name: {}", token.name()); + + // The next call is mutable, but the cost is lower that the deployment, + // so we change the amount of gas + env.set_gas(3_000_000_000u64); + token.transfer(recipient, U256::from(1000)); + + println!("Owner's balance: {:?}", token.balance_of(owner)); + println!("Recipient's balance: {:?}", token.balance_of(recipient)); +} +``` + +:::note +The above example is a rust binary, not a test. Note that it is also added as a section of the +`Cargo.toml` file: +```toml +[bin] +name = "erc20_on_livenet" +path = "src/bin/erc20_on_livenet.rs" +required-features = ["livenet"] +test = false +``` +::: + +## Usage + +To run the above code, we simply need to run the binary with the `livenet` feature enabled: + +```bash +cargo run --bin erc20_on_livenet --features=livenet +``` + +:::note +Before executing the binary, make sure you built a wasm file. +::: + +A part of a sample output should look like this: + +```bash +... +πŸ’ INFO : Calling "hash-d26fcbd210..." with entrypoint "transfer". +πŸ™„ WAIT : Waiting 15s for "65b1a5d21...". +πŸ™„ WAIT : Waiting 15s for "65b1a5d21...". +πŸ’ INFO : Deploy "65b1a5d21..." successfully executed. +Owner's balance: 4004 +Recipient's balance: 4000 +``` +Those logs are a result of the last 4 lines of the above listing. +Each deployment or a call to the blockchain will be noted and will take some time to execute. +We can see that the `transfer` call took over 15 seconds to execute. But calling `balance_of` was nearly instant +and cost us nothing. How it is possible? + +:::info +You can see the deployment on http://cspr.live/ - the transfer from the example +can be seen [here](https://integration.cspr.live/deploy/65b1a5d21174a62c675f89683aba995c453b942c705b404a1f8bbf6f0f6de32a). +::: + +## How Livenet backend works +All calls of entrypoints executed on a Casper blockchain cost gas - even if they do not change the state. +It is possible however to query the state of the blockchain for free. + +This principle is used in the Livenet backend - all calls that do not change the state of the blockchain are really executed offline - the only thing that is requested from the +node is the current state. This is why the `balance_of` call was almost instant and free. + +Basically, if the entrypoint function is not mutable or does not make a call to an unknown external contract +(see [Cross Calls](../basics/10-cross-calls.md)), it is executed offline and +node is used for the state query only. However, the Livenet needs to know the connection between the contracts +and the code, so make sure to deploy or load already deployed contracts + +## Multiple environments + +It is possible to have multiple environments for the Livenet backend. This is useful if we want to easily switch between multiple accounts, +multiple nodes or even multiple chains. + +To do this, simply create a new `.env` file with a different prefix - for example, `integration.env` and `mainnet.env`. +Then, pass the `ODRA_CASPER_LIVENET_ENV` variable with value either `integration` or `mainnet` to select which file +has to be used first. If your `integration.env` file has a value that IS present in the `.env` file, it will +override the value from the `.env` file. + +```bash +ODRA_CASPER_LIVENET_ENV=integration cargo run --bin erc20_on_livenet --features=livenet +``` + +To sum up - this command will firstly load the `integration.env` file and then load the missing values from `.env` file. + +[.env.sample]: https://github.com/odradev/odra/blob/release/1.1.0/examples/.env.sample +[erc20_on_livenet.rs]: https://github.com/odradev/odra/blob/release/1.1.0/examples/bin/erc20_on_livenet.rs \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/backends/_category_.json b/docusaurus/versioned_docs/version-1.2.0/backends/_category_.json new file mode 100644 index 000000000..c903c8698 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/backends/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Backends", + "position": 4, + "link": { + "type": "generated-index", + "description": "Backends" + } +} diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/01-cargo-odra.md b/docusaurus/versioned_docs/version-1.2.0/basics/01-cargo-odra.md new file mode 100644 index 000000000..10634b06d --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/01-cargo-odra.md @@ -0,0 +1,145 @@ +--- +sidebar_position: 1 +description: A tool for managing Odra projects +--- + +# Cargo Odra +If you followed the [Installation](../getting-started/installation.md) tutorial properly, +you should already be set up with the Cargo Odra tool. It is an executable that will help you with +managing your smart contracts project, testing and running them with various configurations. + +Let's take a look at all the possibilities that Cargo Odra gives you. + +## Managing projects + +Two commands help you create a new project. The first one is `cargo odra new`. +You need to pass one parameter, namely `--name {PROJECT_NAME}`: + +```bash +cargo odra new --name my-project +``` + +This creates a new project in the `my_project` folder and name it `my_project`. You can see it +for yourself, for example by taking a look into a `Cargo.toml` file created in your project's folder: + +```toml +[package] +name = "my_project" +version = "0.1.0" +edition = "2021" +``` +The project is created using the template located in [Odra's main repository](https://github.com/odradev/odra). +By default it uses `full` template, if you want, you can use minimalistic `blank` by running: + +```bash +cargo odra new -t blank --name my-project +``` + +The third available template is `workspace`, which creates a workspace with two projects, similar to the one created +with the `full` template. + +By default, the latest release of Odra will be used for the template and as a dependency. +You can pass a source of Odra you want to use, by using `-s` parameter: + +```bash +cargo odra new -n my-project -s ../odra # will use local folder of odra +cargo odra new -n my-project -s release/0.9.0 # will use github branch, e.g. if you want to test new release +cargo odra new -n my-project -s 1.1.0 # will use a version released on crates.io +``` + +The second way of creating a project is by using `init` command: + +```bash +cargo odra init --name my-project +``` + +It works in the same way as `new`, but instead of creating a new folder, it creates a project +in the current, empty directory. + +## Generating code +If you want to quickly create a new contract code, you can use the `generate` command: + +```bash +cargo odra generate -c counter +``` + +This creates a new file `src/counter.rs` with sample code, add appropriate `use` and `mod` sections +to `src/lib.rs` and update the `Odra.toml` file accordingly. To learn more about `Odra.toml` file, +visit [Odra.toml](03-odra-toml.md). + +## Testing +The most used command during the development of your project should be this one: + +```bash +cargo odra test +``` +It runs your tests against Odra's `MockVM`. It is substantially faster than `CasperVM` +and implements all the features Odra uses. + +When you want to run tests against a "real" VM, just provide the name of the backend using `-b` +option: + +```bash +cargo odra test -b casper +``` + +In the example above, Cargo Odra builds the project, generates the wasm files, +spin up `CasperVM` instance, deploys the contracts onto it and runs the tests against it. Pretty neat. + +Keep in mind that this is a lot slower than `OdraVM` and you cannot use the debugger. +This is why `OdraVM` was created and should be your first choice when developing contracts. +Of course, testing all of your code against a blockchain VM is a must in the end. + +If you want to run only some of the tests, you can pass arguments to the `cargo test` command +(which is run in the background obviously): + +```bash +cargo odra test -- this-will-be-passed-to-cargo-test +``` + +If you want to run tests which names contain the word `two`, you can execute: + +```bash +cargo odra test -- two +``` + +Of course, you can do the same when using the backend: + +```bash +cargo odra test -b casper -- two +``` + +## Building code + +You can also build the code itself and generate the output contracts without running the tests. +To do so, simply run: + +```bash +cargo odra build +``` + +If the build process finishes successfully, wasm files will be located in `wasm` folder. +Notice, that this command does not require the `-b` option. + +If you want to build specific contract, you can use `-c` option: + +```bash +cargo odra build -c counter # you pass many comma separated contracts +``` + +## Generating contract schema + +If you want to generate a schema (including the name, entrypoints, events, etc.) for your contract, you can use the `schema` command: + +```bash +cargo odra schema +``` + +This generates a schema file in JSON format for all your contracts and places them in the `resources` folder. +If the `resources` folder does not exist, it creates the folder for you. + +Like with the `build` command, you can use the `-c` option to generate a schema for a specific contract. + +## What's next +In the next section, we will take a look at all the files and directories that `cargo odra` created +for us and explain their purpose. diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/02-directory-structure.md b/docusaurus/versioned_docs/version-1.2.0/basics/02-directory-structure.md new file mode 100644 index 000000000..d2fbac53c --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/02-directory-structure.md @@ -0,0 +1,98 @@ +--- +sidebar_position: 2 +description: Files and folders in the Odra project +--- + +# Directory structure + +After creating a new project using Odra and running the tests, you will be presented with the +following files and directories: + +``` +. +β”œβ”€β”€ Cargo.lock +β”œβ”€β”€ Cargo.toml +β”œβ”€β”€ CHANGELOG.md +β”œβ”€β”€ Odra.toml +β”œβ”€β”€ README.md +β”œβ”€β”€ rust-toolchain +β”œβ”€β”€ src/ +β”‚ β”œβ”€β”€ flipper.rs +β”‚ └── lib.rs +β”œβ”€β”€ bin/ +| |── build_contract.rs +| └── build_schema.rs +β”œβ”€β”€ target/ +└── wasm/ +``` + +## Cargo.toml +Let's first take a look at `Cargo.toml` file: + +```toml +[package] +name = "sample" +version = "0.1.0" +edition = "2021" + +[dependencies] +odra = "1.1.0" + +[dev-dependencies] +odra-test = "1.1.0" + +[build-dependencies] +odra-build = "1.1.0" + +[[bin]] +name = "sample_build_contract" +path = "bin/build_contract.rs" +test = false + +[[bin]] +name = "sample_build_schema" +path = "bin/build_schema.rs" +test = false + +[profile.release] +codegen-units = 1 +lto = true + +[profile.dev.package."*"] +opt-level = 3 +``` + +By default, your project will use the latest odra version available at crates.io. For testing purposes, `odra-test` is also +added as a dev dependency. + +## Odra.toml +This is the file that holds information about contracts that will be generated when running `cargo odra build` and +`cargo odra test`: + +```toml +[[contracts]] +fqn = "sample::Flipper" +``` + +As we can see, we have a single contract, its `fqn` (Fully Qualified Name) corresponds to +the contract is located in `src/flipper.rs`. +More contracts can be added here by hand, or by using `cargo odra generate` command. + +## src/ +This is the folder where your smart contract files live. + +## bin/ +This is the folder where scripts that will be used to generate code or schemas live. +You don't need to modify those files, they are generated by `cargo odra new` command and +are used by `cargo odra build`, `cargo odra test` and `cargo odra schema` commands. + +## target/ +Files generated by cargo during the build process are put here. + +## wasm/ +WASM files generated by `cargo odra build` and `cargo odra test` are put here. You can grab those WASM files +and deploy them on the blockchain. + +# What's next +Now, let's take a look at one of the files mentioned above in more detail, +namely the `Odra.toml` file. diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/03-odra-toml.md b/docusaurus/versioned_docs/version-1.2.0/basics/03-odra-toml.md new file mode 100644 index 000000000..1b6cd71e2 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/03-odra-toml.md @@ -0,0 +1,39 @@ +--- +sidebar_position: 3 +description: Odra's configuration file +--- + +# Odra.toml + +As mentioned in the previous article, `Odra.toml` is a file that contains information about all the contracts +that Odra will build. Let's take a look at the file structure again: + +```toml +[[contracts]] +fqn = "sample::Flipper" +``` + +The `fqn` (Fully Qualified Name) is used by the building tools to locate and build the contract. +The last segment of the `fqn` will be used as the name for your contract - the generated wasm file will +be in the above case named `flipper.wasm`. + + +## Adding a new contract manually + +Besides using the `cargo odra generate` command, you can add a new contract to be compiled by hand. +To do this, add another `[[contracts]]` element, name it and make sure that the `fqn` is set correctly. + +For example, if you want to create a new contract called `counter`, your `Odra.toml` file should finally +look like this: + +```toml +[[contracts]] +fqn = "sample::Flipper" + +[[contracts]] +fqn = "sample::Counter" +``` + +## What's next +In the next section, we'll take a closer look at the code that was generated by Odra by default - the famous +`Flipper` contract. diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/04-flipper-internals.md b/docusaurus/versioned_docs/version-1.2.0/basics/04-flipper-internals.md new file mode 100644 index 000000000..71822bf74 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/04-flipper-internals.md @@ -0,0 +1,115 @@ +--- +sidebar_position: 4 +description: Detailed explanation of the Flipper contract +--- + +# Flipper Internals +In this article, we take a deep dive into the code shown in the +[Flipper example](../getting-started/flipper.md), where we will explain in more detail all +the Odra-specific sections of the code. + +## Header + +```rust title="flipper.rs" +use odra::Var; +``` + +Pretty straightforward. Odra wraps the code of the specific blockchains SDKs into its own implementation +that can be reused between targets. In the above case, we're importing `Var`, which is responsible +for storing simple values on the blockchain's storage. + +## Struct + +```rust title="flipper.rs" +/// A module definition. Each module struct consists of Vars and Mappings +/// or/and other modules. +#[odra::module] +pub struct Flipper { + /// The module itself does not store the value, + /// it's a proxy that writes/reads value to/from the host. + value: Var, +} +``` + +In Odra, all contracts are also modules, which can be reused between contracts. That's why we need +to mark the struct with the `#[odra::module]` attribute. In the struct definition itself, we state all +the fields of the contract. Those fields can be regular Rust data types, however - those will not +be persisted on the blockchain. They can also be Odra modules - defined in your project or coming +from Odra itself. Finally, to make the data persistent on the blockchain, you can use something like +`Var` showed above. To learn more about storage interaction, take a look at the +[next article](05-storage-interaction.md). + +## Impl +```rust title="flipper.rs" +/// Module implementation. +/// +/// To generate entrypoints, +/// an implementation block must be marked as #[odra::module]. +#[odra::module] +impl Flipper { + /// Odra constructor. + /// + /// Initializes the contract with the value of value. + pub fn init(&mut self) { + self.value.set(false); + } + ... +``` +Similarly to the struct, we mark the `impl` section with the `#[odra::module]` attribute. Odra will take all +`pub` functions from this section and create contract endpoints from them. So, if you wish to have +functions that are not available for calling outside the contract, do not make them public. Alternatively, +you can create a separate `impl` section without the attribute - all functions defined there, even marked +with `pub` will be not callable. + +The function named `init` is the constructor of the contract. This function will be limited to only +to a single call, all further calls to it will result in an error. The `init` function is optional, +if your contract does not need any initialization, you can skip it. + +```rust title="flipper.rs" + ... + /// Replaces the current value with the passed argument. + pub fn set(&mut self, value: bool) { + self.value.set(value); + } + + /// Replaces the current value with the opposite value. + pub fn flip(&mut self) { + self.value.set(!self.get()); + } + ... +``` +The endpoints above show you how to interact with the simplest type of storage - `Var`. The data +saved there using `set` function will be persisted in the blockchain. + +## Tests +```rust title="flipper.rs" +#[cfg(test)] +mod tests { + use crate::flipper::FlipperHostRef; + use odra::host::{Deployer, NoArgs}; + + #[test] + fn flipping() { + let env = odra_test::env(); + // To test a module we need to deploy it. Autogenerated `FlipperHostRef` + // implements `Deployer` trait, so we can use it to deploy the module. + let mut contract = FlipperHostRef::deploy(&env, NoArgs); + assert!(!contract.get()); + contract.flip(); + assert!(contract.get()); + } + ... +``` +You can write tests in any way you prefer and know in Rust. In the example above we are deploying the +contract using [`Deployer::deploy`] function called on `FlipperHostRef` - a piece of code generated +by the `#[odra::module]`. Because the module implements the constructor but does not accept any arguments, +as the second argument of the deploy function, we pass `NoArgs` - one of the implementations of +the [`InitArgs`] trait provided with the framework. + +The contract will be deployed on the VM you chose while running `cargo odra test`. + +## What's next +Now let's take a look at the different types of storage that Odra provides and how to use them. + +[`Deployer::deploy`]: https://docs.rs/odra/1.1.0/odra/host/trait.Deployer.html#tymethod.deploy +[`InitArgs`]: https://docs.rs/odra/1.1.0/odra/host/trait.InitArgs.html \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/05-storage-interaction.md b/docusaurus/versioned_docs/version-1.2.0/basics/05-storage-interaction.md new file mode 100644 index 000000000..759a053f6 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/05-storage-interaction.md @@ -0,0 +1,235 @@ +--- +sidebar_position: 5 +description: How to write data into blockchain's storage +--- + +# Storage interaction +The Odra framework implements multiple types of data that can be stored on the blockchain. Let's go +through all of them and explain their pros and cons. + +## Var +The `Var` is the simplest storage type available in the Odra framework. It serializes the data and stores it under a single key in the blockchain storage. To use it, just wrap your +variable in the `Var` type. Let's look at a "real world" example of a contract that represents a dog: + +```rust title="examples/src/features/storage/variable.rs" +use odra::prelude::*; +use odra::Var; + +#[odra::module] +pub struct DogContract { + barks: Var, + weight: Var, + name: Var, + walks: Var>, +} +``` + +You can see the `Var` wrapping the data. Even complex types like `Vec` can be wrapped (with some caveats)! + +Let's make this contract usable, by providing a constructor and some getter functions: + +```rust title="examples/src/features/storage/variable.rs" +#[odra::module] +impl DogContract { + pub fn init(&mut self, barks: bool, weight: u32, name: String) { + self.barks.set(barks); + self.weight.set(weight); + self.name.set(name); + self.walks.set(Vec::::default()); + } + + pub fn barks(&self) -> bool { + self.barks.get_or_default() + } + + pub fn weight(&self) -> u32 { + self.weight.get_or_default() + } + + pub fn name(&self) -> String { + self.name.get_or_default() + } + + pub fn walks_amount(&self) -> u32 { + let walks = self.walks.get_or_default(); + walks.len() as u32 + } + + pub fn walks_total_length(&self) -> u32 { + let walks = self.walks.get_or_default(); + walks.iter().sum() + } +} +``` + +As you can see, you can access the data, by using `get_or_default` function: + +```rust title="examples/src/features/storage/variable.rs" +... +self.barks.get_or_default() +... +``` + +:::note +Keep in mind that using `get()` will result in an Option that you'll need to unwrap - the variable +doesn't have to be initialized! +::: + +To modify the data, use the `set()` function: + +```rust title="examples/src/features/storage/variable.rs" +self.barks.set(barks); +``` + +A `Var` is easy to use and efficient for simple data types. One of its downsides is that it +serializes the data as a whole, so when you're using complex types like `Vec` or `HashMap`, +each time you `get` or `set` the whole data is read and written to the blockchain storage. + +In the example above, if we want to see how many walks our dog had, we would use the function: +```rust title="examples/src/features/storage/variable.rs" +pub fn walks_amount(&self) -> usize { + let walks = self.walks.get_or_default(); + walks.len() +} +``` +But to do so, we need to extract the whole serialized vector from the storage, which would inefficient, +especially for larger sets of data. + +To tackle this issue following two types were created. + +## Mapping + +The `Mapping` is used to store and access data as key-value pairs. To define a `Mapping`, you need to +pass two values - the key type and the value type. Let's look at the variation of the Dog contract, that +uses `Mapping` to store information about our dog's friends and how many times they visited: + +```rust title="examples/src/features/storage/mapping.rs" +use odra::prelude::*; +use odra::{Mapping, Var}; + +#[odra::module] +pub struct DogContract2 { + name: Var, + friends: Mapping, +} +``` + +In the example above, our key is a String (it is a name of the friend) and we are storing u32 values +(amount of visits). To read and write values from and into a `Mapping` we use a similar approach +to the one shown in the Vars section with one difference - we need to pass a key: + +```rust title="examples/src/features/storage/mapping.rs" +pub fn visit(&mut self, friend_name: String) { + let visits = self.visits(friend_name.clone()); + self.friends.set(&friend_name, visits + 1); +} + +pub fn visits(&self, friend_name: String) -> u32 { + self.friends.get_or_default(&friend_name) +} +``` + +The biggest improvement over a `Var` is that we can model functionality of a `HashMap` using `Mapping`. +The amount of data written to and read from the storage is minimal. However, we cannot iterate over `Mapping`. +We could implement such behavior by using a numeric type key and saving the length of the set in a +separate variable. Thankfully Odra comes with a prepared solution - the `List` type. + +:::note +If you take a look into List implementation in Odra, you'll see that in fact it is just a Mapping with +a Var working together: + +```rust title="core/src/list.rs" +use odra::{List, Var}; + +pub struct List { + values: Mapping, + index: Var +} +``` +::: + +## List +Going back to our DogContract example - let's revisit the walk case. This time, instead of `Vec`, +we'll use the list: + +```rust title="examples/src/features/storage/list.rs" +use odra::{prelude::*, List, Var}; + +#[odra::module] +pub struct DogContract3 { + name: Var, + walks: List, +} +``` + +As you can see, the notation is very similar to the `Vec`. To understand the usage, take a look +at the reimplementation of the functions with an additional function that takes our dog for a walk +(it writes the data to the storage): + +```rust title="examples/src/features/storage/list.rs" +#[odra::module] +impl DogContract3 { + pub fn init(&mut self, name: String) { + self.name.set(name); + } + + pub fn name(&self) -> String { + self.name.get_or_default() + } + + pub fn walks_amount(&self) -> u32 { + self.walks.len() + } + + pub fn walks_total_length(&self) -> u32 { + self.walks.iter().sum() + } + + pub fn walk_the_dog(&mut self, length: u32) { + self.walks.push(length); + } +} +``` + +Now, we can know how many walks our dog had without loading the whole vector from the storage. +We need to do this to sum the length of all the walks, but the Odra framework cannot (yet) handle all +the cases for you. + +:::info +All of the above examples, alongside the tests, are available in the Odra repository in the `examples/src/features/` folder. +::: + +## Custom Types + +By default you can store only built-in types like numbers, Options, Results, Strings, Vectors. + +Implementing custom types is straightforward, your type must add `#[odra::odra_type]` attribute. Let's see how to implement a `Dog` type: + +```rust +use odra::Address; + +#[odra::odra_type] +pub struct Dog { + pub name: String, + pub age: u8, + pub owner: Option
+} +``` + +`#[odra_type]` is applicable to named field structs and enums. It generates serialization, deserialization and schema code for your type. +`CLType` of a custom type is `CLType::Any`, except for an unit-only enum, which is `CLType::U8`. + +```rust title="unit_only_enum.rs" +enum Enum { + Foo = 3, + Bar = 2, + Baz = 1, +} +``` + +:::note +Each custom typed field of your struct must be marked with the `#[odra::odra_type]` attribute . +::: + +## What's next +In the next article, we'll see how to query the host for information about the world and our contract. diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/06-communicating-with-host.md b/docusaurus/versioned_docs/version-1.2.0/basics/06-communicating-with-host.md new file mode 100644 index 000000000..56449105c --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/06-communicating-with-host.md @@ -0,0 +1,54 @@ +--- +sidebar_position: 6 +description: How to get information from the Host +--- + +# Host Communication + +One of the things that your contract will probably do is to query the host for some information - +what is the current time? Who called me? Following example shows how to do this: + +```rust title="examples/src/features/host_functions.rs" +use odra::prelude::*; +use odra::{Address, Var}; + +#[odra::module] +pub struct HostContract { + name: Var, + created_at: Var, + created_by: Var
+} + +#[odra::module] +impl HostContract { + pub fn init(&mut self, name: String) { + self.name.set(name); + self.created_at.set(self.env().get_block_time()); + self.created_by.set(self.env().caller()) + } + + pub fn name(&self) -> String { + self.name.get_or_default() + } +} +``` + +As you can see, we are using `self.env()`. It is an implementation of [`Module::env()`], autogenerated +by `#[odra::module]` attribute. The function returns a reference to the [`ContractEnv`] (you can read more in +the [`Backend section`]). This is a structure that provides access to the host functions and variables. + +In this example, we use two of them: +* `get_block_time()` - returns the current block time as u64. +* `caller()` - returns an Odra `Address` of the caller (this can be an external caller or another contract). + +:::info +You will learn more functions that Odra exposes from host and types it uses in further articles. +::: + +## What's next +In the next article, we'll dive into testing your contracts with Odra, so you can check that the code +we presented in fact works! + +[`Module::env()`]: https://docs.rs/odra/1.1.0/odra/module/trait.Module.html#tymehtod.env +[`ContractEnv`]: https://docs.rs/odra/1.1.0/odra/struct.ContractEnv.html +[`Backend section`]: ../backends/01-what-is-a-backend.md#contract-env \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/07-testing.md b/docusaurus/versioned_docs/version-1.2.0/basics/07-testing.md new file mode 100644 index 000000000..f841f511b --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/07-testing.md @@ -0,0 +1,135 @@ +--- +sidebar_position: 7 +description: How to write tests in Odra +--- + +# Testing +Thanks to the Odra framework, you can test your code in any way you are used to. This means you can write +regular Rust unit and integration tests. Have a look at how we test the Dog Contract we created in the +previous article: + +```rust title="examples/src/features/storage/list.rs" +use odra::{List, Var}; + +#[cfg(test)] +mod tests { + use super::{DogContract3HostRef, DogContract3InitArgs}; + use odra::{host::Deployer, prelude::*}; + + #[test] + fn init_test() { + let test_env = odra_test::env(); + let init_args = DogContract3InitArgs { + name: "DogContract".to_string() + }; + let mut dog_contract = DogContract3HostRef::deploy(&test_env, init_args); + assert_eq!(dog_contract.walks_amount(), 0); + assert_eq!(dog_contract.walks_total_length(), 0); + dog_contract.walk_the_dog(5); + dog_contract.walk_the_dog(10); + assert_eq!(dog_contract.walks_amount(), 2); + assert_eq!(dog_contract.walks_total_length(), 15); + } +} +``` + +The first interesting thing you may notice is placed the import section. + +```rust +use super::{DogContract3HostRef, DogContract3InitArgs}; +use odra::{host::Deployer, prelude::*}; +``` + +We are using `super` to import the `DogContract3HostRef` and `DogContract3InitArgs` from the parent module. +`{{ModuleName}}HostRef` and `{{ModuleName}}InitArgs` are types that was generated for us by Odra. + +`DogContract3HostRef` is a reference to the contract that we can use to interact with it (call entrypoints) +and implements [`HostRef`] trait. + +`DogContract3InitArgs` is a struct that we use to initialize the contract and implements [`InitArgs`] trait. +Considering the contract initialization, there three possible scenarios: +1. The contract has a constructor with arguments, then Odra creates a struct named `{{ModuleName}}InitArgs`. +2. The contract has a constructor with no arguments, then you can use `odra::host::NoArgs`. +3. The contract does not have a constructor, then you can use `odra::host::NoArgs`. +All of those structs implement the `odra::host::InitArgs` trait, required to conform to the +`Deployer::deploy` method signature. + +The other import is `odra::host::Deployer`. This is a trait is used to deploy the contract and give us a reference to it. + +Let's take a look at the test itself. How to obtain a reference to the contract? + `{{ModuleName}}HostRef` implements the [`Deployer`] trait, which provides the `deploy` method: + +```rust title="examples/src/features/storage/list.rs" +let mut dog_contract = DogContract3HostRef::deploy(&test_env, init_args); +``` + +From now on, we can use `dog_contract` to interact with our deployed contract - in particular, all +`pub` functions from the impl section that was annotated with the `odra::module` attribute are available to us: + +```rust title="examples/src/features/storage/list.rs" +// Impl +pub fn walk_the_dog(&mut self, length: u32) { + self.walks.push(length); +} + +... + +// Test +dog_contract.walk_the_dog(5); +``` + +## HostEnv + +Odra gives us some additional functions that we can use to communicate with the host (outside the contract context) +and to configure how the contracts are deployed and called. Let's revisit the example from the previous +article about host communication and implement the tests that prove it works: + +```rust title="examples/src/features/testing.rs" +#[cfg(test)] +mod tests { + use crate::features::testing::{TestingContractHostRef, TestingContractInitArgs}; + use odra::{host::{Deployer, HostEnv}, prelude::*}; + + #[test] + fn env() { + let test_env: HostEnv = odra_test::env(); + test_env.set_caller(test_env.get_account(0)); + let init_args = TestingContractInitArgs { + name: "MyContract".to_string() + }; + let testing_contract = TestingContractHostRef::deploy(&test_env, init_args); + let creator = testing_contract.created_by(); + test_env.set_caller(test_env.get_account(1)); + let init_args = TestingContractInitArgs { + name: "MyContract2".to_string() + }; + let testing_contract2 = TestingContractHostRef::deploy(&test_env, init_args); + let creator2 = testing_contract2.created_by(); + assert_ne!(creator, creator2); + } +} +``` +In the code above, at the beginning of the test, we are obtaining a `HostEnv` instance using `odra_test::env()`. +Next, we are deploying two instances of the same contract, but we're using `HostEnv::set_caller` +to change the caller - so the Address which is deploying the contract. This changes the result of the `odra::ContractEnv::caller()` +the function we are calling inside the contract. + +`HostEnv` comes with a set of functions that will let you write better tests: + +- `fn set_caller(&self, address: Address)` - you've seen it in action just now +- `fn balance_of(&self, address: &Address) -> U512` - returns the balance of the account associated with the given address +- `fn advance_block_time(&self, time_diff: u64)` - increases the current value of `block_time` +- `fn get_account(&self, n: usize) -> Address` - returns an n-th address that was prepared for you by Odra in advance; + by default, you start with the 0-th account +- `fn emitted_event(&self, contract_address: &R, event: &T) -> bool` - verifies if the event was emitted by the contract + +Full list of functions can be found in the [`HostEnv`] documentation. + +## What's next +We take a look at how Odra handles errors! + +[`HostRef`]: https://docs.rs/odra/1.1.0/odra/host/trait.HostRef.html +[`InitArgs`]: https://docs.rs/odra/1.1.0/odra/host/trait.InitArgs.html +[`HostEnv`]: https://docs.rs/odra/1.1.0/odra/host/struct.HostEnv.html +[`Deployer`]: https://docs.rs/odra/1.1.0/odra/host/trait.Deployer.html +[`HostEnv`]: https://docs.rs/odra/1.1.0/odra/host/struct.HostEnv.html \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/08-errors.md b/docusaurus/versioned_docs/version-1.2.0/basics/08-errors.md new file mode 100644 index 000000000..384895ab1 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/08-errors.md @@ -0,0 +1,122 @@ +--- +sidebar_position: 8 +description: Causing and handling errors +--- + +# Errors + +Odra comes with tools that allow you to throw, handle and test for errors in execution. Take a look at the +following example of a simple owned contract: + +```rust title="examples/src/features/handling_errors.rs" +use odra::prelude::*; +use odra::{Address, Var}; + +#[odra::module(errors = Error)] +pub struct OwnedContract { + name: Var, + owner: Var
+} + +#[odra::odra_error] +pub enum Error { + OwnerNotSet = 1, + NotAnOwner = 2 +} + +#[odra::module] +impl OwnedContract { + pub fn init(&mut self, name: String) { + self.name.set(name); + self.owner.set(self.env().caller()) + } + + pub fn name(&self) -> String { + self.name.get_or_default() + } + + pub fn owner(&self) -> Address { + self.owner.get_or_revert_with(Error::OwnerNotSet) + } + + pub fn change_name(&mut self, name: String) { + let caller = self.env().caller(); + if caller != self.owner() { + self.env().revert(Error::NotAnOwner) + } + + self.name.set(name); + } +} +``` + +Firstly, we are using the `#[odra::odra_error]` attribute to define our own set of Errors that our contract will +throw. Then, you can use those errors in your code - for example, instead of forcefully unwrapping Options, you can use +`unwrap_or_revert_with` and pass an error as an argument: + +```rust title="examples/src/features/handling_errors.rs" +self.owner.get().unwrap_or_revert_with(Error::OwnerNotSet) +``` + +You can also throw the error directly, by using `revert`: + +```rust title="examples/src/features/handling_errors.rs" +self.env().revert(Error::NotAnOwner) +``` + +To register errors, add the `errors` inner attribute to the struct's `#[odra::module]` attribute and pass the error type as the value. The registered errors will be present in the contract [`schema`]. + +Defining an error in Odra, you must keep in mind a few rules: + +1. An error should be a field-less enum. +2. The enum must be annotated with `#[odra::odra_error]`. +3. Avoid implicit discriminants. + +:::note +In your project you can define as many error enums as you wish, but you must ensure that the discriminants are unique across the project! +::: + +## Testing errors + +Okay, but how about testing it? Let's write a test that will check if the error is thrown when the caller is not an owner: + +```rust title="examples/src/features/handling_errors.rs" +#[cfg(test)] +mod tests { + use super::{Error, OwnedContractHostRef, OwnedContractInitArgs}; + use odra::{host::Deployer, prelude::*}; + + #[test] + fn test_owner_error() { + let test_env = odra_test::env(); + let owner = test_env.get_account(0); + let not_an_owner = test_env.get_account(1); + + test_env.set_caller(owner); + let init_args = OwnedContractInitArgs { + name: "OwnedContract".to_string() + }; + let mut owned_contract = OwnedContractHostRef::deploy(&test_env, init_args); + + test_env.set_caller(not_an_owner); + assert_eq!( + owned_contract.try_change_name("NewName".to_string()), + Err(Error::NotAnOwner.into()) + ); + } +} +``` +Each `{{ModuleName}}HostRef` has `try_{{entry_point_name}}` functions that return an [`OdraResult`]. +`OwnedContractHostRef` implements regular entrypoints: `name`, `owner`, `change_name`, and +and safe its safe version: `try_name`, `try_owner`, `try_change_name`. + +In our example, we are calling `try_change_name` and expecting an error to be thrown. +For assertions, we are using a standard `assert_eq!` macro. As the contract call returns an `OdraError`, +we need to convert our custom error to `OdraError` using `Into::into()`. + +## What's next +We will learn how to emit and test events using Odra. + +[`OdraResult`]: https://docs.rs/odra/1.1.0/odra/type.OdraResult.html +[`OdraError`]: https://docs.rs/odra/1.1.0/odra/enum.OdraError.html +[`schema`]: ./casper-contract-schema \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/09-events.md b/docusaurus/versioned_docs/version-1.2.0/basics/09-events.md new file mode 100644 index 000000000..5df759caa --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/09-events.md @@ -0,0 +1,90 @@ +--- +sidebar_position: 9 +description: Creating and emitting Events +--- + +# Events + +In the EVM world events are stored as logs within the blockchain's transaction receipts. These logs can be accessed by external applications or other smart contracts to monitor and react to specific events. Casper does not support events natively, however, Odra mimics this feature. Take a look: + +```rust title="examples/src/features/events.rs" +use odra::prelude::*; +use odra::Address; + +#[odra::module(events = [PartyStarted])] +pub struct PartyContract; + +#[odra::event] +pub struct PartyStarted { + pub caller: Address, + pub block_time: u64 +} + +#[odra::module] +impl PartyContract { + pub fn init(&self) { + self.env().emit_event(PartyStarted { + caller: self.env().caller(), + block_time: self.env().get_block_time() + }); + } +} +``` + +We defined a new contract, which emits an event called `PartyStarted` when the contract is deployed. +To define an event, add the `#[odra::event]` attribute like this: + +```rust title="examples/src/features/events.rs" +#[odra::event] +pub struct PartyStarted { + pub caller: Address, + pub block_time: u64, +} +``` + +To emit an event, we use the `emit_event` function from the `ContractEnv`, passing the event as an argument: + +```rust title="examples/src/features/events.rs" +self.env().emit_event(PartyStarted { + caller: self.env().caller(), + block_time: self.env().get_block_time() +}); +``` + +To determine all the events at compilation time to register them once the contract is deployed. To register events, add an `events` inner attribute to the struct's `#[odra::module]` attribute. The registered events will also be present in the contract [`schema`]. + +The event collection process is recursive; if your module consists of other modules, and they have already registered their events, you don't need to add them to the parent module. + +## Testing events + +Odra's `HostEnv` comes with a few functions which lets you easily test the events that a given contract has emitted: + +```rust title="examples/src/features/events.rs" +use super::{PartyContractHostRef, PartyStarted}; +use odra::host::{Deployer, HostEnv, NoArgs}; + +#[test] +fn test_party() { + let test_env: HostEnv = odra_test::env(); + let party_contract = PartyContractHostRef::deploy(&test_env, NoArgs); + test_env.emitted_event( + &party_contract, + &PartyStarted { + caller: test_env.get_account(0), + block_time: 0 + } + ); + // If you do not want to check the exact event, you can use `emitted` function + test_env.emitted(&party_contract, "PartyStarted"); + // You can also check how many events were emitted. + assert_eq!(test_env.events_count(&party_contract), 1); +} +``` + +To explore more event testing functions, check the [`HostEnv`] documentation. + +## What's next +Read the next article to learn how to call other contracts from the contract context. + +[`HostEnv`]: https://docs.rs/odra/1.1.0/odra/host/struct.HostEnv.html +[`schema`]: ./casper-contract-schema \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/10-cross-calls.md b/docusaurus/versioned_docs/version-1.2.0/basics/10-cross-calls.md new file mode 100644 index 000000000..f8b0ac776 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/10-cross-calls.md @@ -0,0 +1,163 @@ +--- +sidebar_position: 11 +description: Contracts calling contracts +--- + +# Cross calls + +To show how to handle calls between contracts, first, let's implement two of them: + +```rust title="examples/src/features/cross_calls.rs" +use odra::{prelude::*, Address, External}; + +#[odra::module] +pub struct CrossContract { + pub math_engine: External +} + +#[odra::module] +impl CrossContract { + pub fn init(&mut self, math_engine_address: Address) { + self.math_engine.set(math_engine_address); + } + + pub fn add_using_another(&self) -> u32 { + self.math_engine.add(3, 5) + } +} + +#[odra::module] +pub struct MathEngine; + +#[odra::module] +impl MathEngine { + pub fn add(&self, n1: u32, n2: u32) -> u32 { + n1 + n2 + } +} +``` +`MathEngine` contract can add two numbers. `CrossContract` takes an `Address` in its `init` function and saves it in +storage for later use. If we deploy the `MathEngine` first and take note of its address, we can then deploy +`CrossContract` and use `MathEngine` to perform complicated calculations for us! + +To perform a cross-contact call, we use the `External` module component and wrap the `{{ModuleName}}ContractRef` +that was created for us by Odra: + +```rust title="examples/src/features/cross_calls.rs" +pub struct CrossContract { + pub math_engine: External +} +``` + +and then we use the `math_engine` like any other contract/module: + +```rust title="examples/src/features/cross_calls.rs" +self.math_engine.add(3, 5) +``` + +Alternatively, we could store a raw `Address`, then use the `{{ModuleName}}ContractRef` directly: + +```rust title="examples/src/features/cross_calls.rs" +MathEngineContractRef::new(self.env(), math_engine_address).add(3, 5) +``` + +## Contract Ref +We mentioned `HostRef` already in our [Testing](07-testing.md) article - a host side reference to already deployed contract. + +In the module context we use a `ContractRef` instead, to call other contracts. + +Similarly to the `{{ModuleName}}HostRef`, the `{{ModuleName}}ContractRef` is generated automatically, +by the `#[odra::module]` attribute. + +The reference implements all the public endpoints to the contract (those marked as `pub` in `#[odra::module]` +impl), and the `{{ModuleName}}ContractRef::address()` function, which returns the address of the contract. + +# External Contracts +Sometimes in our contract, we would like to interact with a someone else's contract, already deployed onto the blockchain. The only thing we know about the contract is the ABI. + +For that purpose, we use `#[odra:external_contract]` attribute. This attribute should be applied to a trait. The trait defines the part of the ABI we would like to take advantage of. + +Let's pretend the `MathEngine` we defined is an external contract. There is a contract with `add()` function that adds two numbers somewhere. + +```rust +#[odra::external_contract] +pub trait Adder { + fn add(&self, n1: u32, n2: u32) -> u32; +} +``` + +Analogously to modules, Odra creates the `AdderContractRef` struct (and `AdderHostRef` to be used in tests, but do not implement the `Deployer` trait). Having an address, in the module context we can call: + +```rust +struct Contract { + adder: External +} +// in some function +self.adder.add(3, 5) + +// or + +struct Contract { + adder: Var
+} +// in some function +AdderContractRef::new(self.env(), address).add(3, 5) +``` + +### Loading the contract +Sometimes it is useful to load the deployed contract instead of deploying it by ourselves. This is especially useful when we want to test +our contracts in [Livenet](../backends/04-livenet.md) backend. We can load the contract using `load` method on the `Deployer`: + +```rust title="examples/bin/erc20_on_livenet.rs" +fn _load(env: &HostEnv) -> Erc20HostRef { + let address = "hash-d26fcbd2106e37be975d2045c580334a6d7b9d0a241c2358a4db970dfd516945"; + let address = Address::from_str(address).unwrap(); + ::load(env, address) +} +``` + +## Testing +Let's see how we can test our cross calls using this knowledge: + +```rust title="examples/src/features/cross_calls.rs" +#[cfg(test)] +mod tests { + use super::{CrossContractHostRef, CrossContractInitArgs, MathEngineHostRef}; + use odra::host::{Deployer, HostRef, NoArgs}; + + #[test] + fn test_cross_calls() { + let test_env = odra_test::env(); + let math_engine_contract = MathEngineHostRef::deploy(&test_env, NoArgs); + let cross_contract = CrossContractHostRef::deploy( + &test_env, + CrossContractInitArgs { + math_engine_address: *math_engine_contract.address() + } + ); + assert_eq!(cross_contract.add_using_another(), 8); + } +} +``` + +Each test begins with a clean instance of the blockchain, with no contracts deployed. To test an external contract, we first deploy a `MathEngine` contract, although we won't directly utilize it. Instead, we only extract its address. Let's continue assuming there is a contract featuring the `add()` function that we intend to utilize. + +```rust +#[cfg(test)] +mod tests { + use super::*; + use odra::{Address, host::{Deployer, HostRef, NoArgs}}; + + #[test] + fn test_ext() { + let test_env = odra_test::env(); + let adder = AdderHostRef::new(&test_env, get_adder_address(&test_env)).add(3, 5) + assert_eq!(adder.add(1, 2), 3); + } + + fn get_adder_address(test_env: &HostEnv) -> Address { + let contract = MathEngineHostRef::deploy(test_env, NoArgs); + *contract.address() + } +} +``` diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/11-modules.md b/docusaurus/versioned_docs/version-1.2.0/basics/11-modules.md new file mode 100644 index 000000000..f1a1a1f2a --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/11-modules.md @@ -0,0 +1,64 @@ +--- +sidebar_position: 12 +description: Divide your code into modules +--- + +# Modules + +Simply put, modules in Odra let you reuse your code between contracts or even projects. Every contract you +write is also a module, thanks to the `#[odra::module]` attribute. This means that we can easily rewrite our math +example from the previous article, to use a single contract, but still separate our "math" code: + +```rust title="examples/src/features/modules.rs" +use crate::features::cross_calls::MathEngine; +use odra::module::SubModule; +use odra::prelude::*; + +#[odra::module] +pub struct ModulesContract { + pub math_engine: SubModule +} + +#[odra::module] +impl ModulesContract { + pub fn add_using_module(&self) -> u32 { + self.math_engine.add(3, 5) + } +} +``` + +:::important +To use a module as a component of another module, you need to use the `SubModule` type. This is a special type +that crates a keyspace (read more in [Storage Layout]) and provide access to its public methods. +::: + +Note that we didn't need to rewrite the `MathEngine` - we are using the contract from cross calls example as +a module! + +:::info +To see how modules can be used in a real-world scenario, check out the [OwnedToken example] in the main Odra repository! +::: + +## Testing +As we don't need to hold addresses, the test is really simple: + +```rust title="examples/src/features/modules.rs" +#[cfg(test)] +mod tests { + use super::ModulesContractHostRef; + use odra::host::{Deployer, NoArgs}; + + #[test] + fn test_modules() { + let test_env = odra_test::env(); + let modules_contract = ModulesContractHostRef::deploy(&test_env, NoArgs); + assert_eq!(modules_contract.add_using_module(), 8); + } +} +``` + +## What's next +We will see how to handle native token transfers. + +[OwnedToken example]: https://github.com/odradev/odra/blob/release/1.1.0/examples/src/contracts/owned_token.rs +[Storage Layout]: ../advanced/04-storage-layout.md \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/12-native-token.md b/docusaurus/versioned_docs/version-1.2.0/basics/12-native-token.md new file mode 100644 index 000000000..b0c79422d --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/12-native-token.md @@ -0,0 +1,77 @@ +--- +sidebar_position: 13 +description: How to deposit, withdraw and transfer +--- + +# Native token +Different blockchains come with different implementations of their native tokens. Odra wraps it all for you +in easy-to-use code. Let's write a simple example of a public wallet - a contract where anyone can deposit +their funds and anyone can withdraw them: + +```rust title="examples/src/features/native_token.rs" +use odra::prelude::*; +use odra::{casper_types::U512, module::Module}; + +#[odra::module] +pub struct PublicWallet; + +#[odra::module] +impl PublicWallet { + #[odra(payable)] + pub fn deposit(&mut self) {} + + pub fn withdraw(&mut self, amount: &U512) { + self.env().transfer_tokens(&self.env().caller(), amount); + } +} +``` + +:::warning +The code above works, but is dangerous and unfinished - besides allowing you to lose your funds to anyone, it doesn't make +any checks. To keep the code simple, we skipped the part, where the contract checks if the transfer is +even possible. + +To see a more reasonable example, check out `examples/src/contracts/tlw.rs` in the odra main repository. +::: + +You can see a new attribute used here: `#[odra(payable)]` - it will add all the code needed for a function to +be able to receive the funds. Additionally, we are using a new function from `ContractEnv::transfer_tokens()`. +It does exactly what you are expecting it to do - it transfers native tokens from the contract to the +specified address. + +## Testing +To be able to test how many tokens a contract (or any address) has, `HostEnv` comes with a function - +`balance_of`: + +```rust title="examples/src/features/native_token.rs" +#[cfg(test)] +mod tests { + use super::PublicWalletHostRef; + use odra::{casper_types::U512, host::{Deployer, HostRef, NoArgs}}; + + #[test] + fn test_modules() { + let test_env = odra_test::env(); + let mut my_contract = PublicWalletHostRef::deploy(&test_env, NoArgs); + assert_eq!(test_env.balance_of(my_contract.address()), U512::zero()); + + my_contract.with_tokens(U512::from(100)).deposit(); + assert_eq!(test_env.balance_of(my_contract.address()), U512::from(100)); + + my_contract.withdraw(U512::from(25)); + assert_eq!(test_env.balance_of(my_contract.address()), U512::from(75)); + } +} +``` + +## HostEnv +In a broader context of the host environment (test, livenet), you can also transfer `CSPR` tokens between accounts: + +```rust showLineNumbers +let env = odra_casper_livenet_env::env(); +//let env = odra_test::env(); +let (alice, bob) = (env.get_account(0), env.get_account(1)); + +env.set_caller(alice); +let result = env.transfer_tokens(bob, odra::casper_types::U512::from(100)); +``` diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/13-casper-contract-schema.md b/docusaurus/versioned_docs/version-1.2.0/basics/13-casper-contract-schema.md new file mode 100644 index 000000000..059093e06 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/13-casper-contract-schema.md @@ -0,0 +1,265 @@ +--- +sidebar_position: 10 +description: Casper Contract Schema +--- + +# Casper Contract Schema + + Working in collaboration with the Casper Association we designed the [Casper Contract Schema] (CCS). This a standardize description of smart contracts. This is a crucial step enhancing tool development and increasing ecosystem interoperability. + +## Odra and CCS + +There is almost nothing you need to do to use CCS in your Odra project. The only thing to be taken care of is using odra attributes namely: `module`, `event`, `odra_error` and `odra_type`. The schema will be generated for you and available in the `resources` directory. + + +:::note +If you forget to register events and errors in the module attribute, the definition remains valid; however, the errors and events will not be incorporated into the schema. +::: + +```rust showLineNumbers title="src/contract.rs" +use odra::prelude::*; +use odra::{Address, Var}; + +#[odra::module( + // the name of the contract, default is the module name + name = "MyContract", + // the version of the contract, default is the version of the crate + version = "0.1.0", + // events that the contract can emit, collected recursively if submodules are used + events = [ + Created, + Updated + ], + // the error enum the contract can revert with, collected recursively if submodules are used + errors = MyErrors +)] +pub struct MyContract { + name: Var, + owner: Var
, +} + +#[odra::module] +impl MyContract { + /// Initializes the contract, sets the name and owner and emits an event + pub fn init(&mut self, name: String, owner: Address) { + self.name.set(name.clone()); + self.owner.set(owner.clone()); + self.env().emit_event(Created { name }); + } + + /// Updates the name of the contract and emits an event + pub fn update(&mut self, name: String) { + self.name.set(name.clone()); + self.env().emit_event(Updated { name }); + } + + /// Returns the data of the contract + pub fn get_data(&self) -> Data { + Data { + name: self.name.get_or_default(), + owner: self.owner.get_or_revert_with(MyErrors::InvalidOwner), + } + } +} + +// The struct will we visible in the schema in the types section +#[odra::odra_type] +pub struct Data { + name: String, + owner: Address, +} + +// The enum variants will we visible in the schema in the errors section +#[odra::odra_error] +pub enum MyErrors { + /// The owner is invalid + InvalidOwner, + /// The name is invalid + InvalidName, +} + +// The struct will we visible in the schema in the types and events section +#[odra::event] +pub struct Updated { + name: String, +} + +// The struct will we visible in the schema in the types section and events section +#[odra::event] +pub struct Created { + name: String, +} +``` + + +## Generating the Schema + +To generate the schema run the following `cargo-odra` command: + +```bash +cargo odra schema # or pass -c flag to generate the schema for a specific contract +``` + +## Schema Output + +The generated schema will be available in the `resources` directory. The schema is a JSON file that contains all the information about the contract. Here is an example of the generated schema: + +```json showLineNumbers title="resources/my_contract_schema.json" +{ + "casper_contract_schema_version": 1, + "toolchain": "rustc 1.77.0-nightly (5bd5d214e 2024-01-25)", + "authors": [], + "repository": null, + "homepage": null, + "contract_name": "MyContract", + "contract_version": "0.1.0", + "types": [ + { + "struct": { + "name": "Created", + "description": null, + "members": [ + { + "name": "name", + "description": null, + "ty": "String" + } + ] + } + }, + { + "struct": { + "name": "Data", + "description": null, + "members": [ + { + "name": "name", + "description": null, + "ty": "String" + }, + { + "name": "owner", + "description": null, + "ty": "Key" + } + ] + } + }, + { + "struct": { + "name": "Updated", + "description": null, + "members": [ + { + "name": "name", + "description": null, + "ty": "String" + } + ] + } + } + ], + "errors": [ + { + "name": "InvalidName", + "description": "The name is invalid", + "discriminant": 1 + }, + { + "name": "InvalidOwner", + "description": "The owner is invalid", + "discriminant": 0 + } + ], + "entry_points": [ + { + "name": "update", + "description": "Updates the name of the contract and emits an event", + "is_mutable": true, + "arguments": [ + { + "name": "name", + "description": null, + "ty": "String", + "optional": false + } + ], + "return_ty": "Unit", + "is_contract_context": true, + "access": "public" + }, + { + "name": "get_data", + "description": "Returns the data of the contract", + "is_mutable": false, + "arguments": [], + "return_ty": "Data", + "is_contract_context": true, + "access": "public" + } + ], + "events": [ + { + "name": "Created", + "ty": "Created" + }, + { + "name": "Updated", + "ty": "Updated" + } + ], + "call": { + "wasm_file_name": "MyContract.wasm", + "description": "Initializes the contract, sets the name and owner and emits an event", + "arguments": [ + { + "name": "odra_cfg_package_hash_key_name", + "description": "The arg name for the package hash key name.", + "ty": "String", + "optional": false + }, + { + "name": "odra_cfg_allow_key_override", + "description": "The arg name for the allow key override.", + "ty": "Bool", + "optional": false + }, + { + "name": "odra_cfg_is_upgradable", + "description": "The arg name for the contract upgradeability setting.", + "ty": "Bool", + "optional": false + }, + { + "name": "name", + "description": null, + "ty": "String", + "optional": false + }, + { + "name": "owner", + "description": null, + "ty": "Key", + "optional": false + } + ] + } +} +``` + + +## Schema Fields + +* `casper_contract_schema_version` is the version of the schema. +`toolchain` is the version of the Rust compiler used to compile the contract. +* Fields `authors`, `repository`, and `homepage` are optional and can be set in the `Cargo.toml` file. +* `contract_name` is the name of the contract - by default is the module name, may be overriden by the module attribute. +* `contract_version` denotes the version of the contract, defaulting to the version specified in the `Cargo.toml` file, but can be overridden by the `module` attribute. +* `types` comprises a list of custom structs and enums defined within the contract. Each struct or enum includes a name, description (not currently supported, with the value set to `null`), and a list of members. +* `errors` is a list of error enums defined within the contract. Each error includes a name, description (the first line of the variant documentation), and a discriminant. +* `entry_points` is a list of contract functions that can be called from the outside. Each entry point includes a name, description (not currently supported, with the value set to `null`), whether the function is mutable, a list of arguments, the return type, whether the function is called in the contract context, and the access level. +* `events` is a list of events that the contract can emit. Each event includes a name and the type (earlier defined in `types`) of the event. +* The `call` section provides details regarding the contract's `call` function, which executes upon contract deployment. It includes the name of the Wasm file, a description (reflecting the constructor's description, typically the `init` function), and a list of arguments. These arguments are a combination of Odra configuration arguments and constructor arguments. + + + [Casper Contract Schema]: https://github.com/odradev/casper-contract-schema \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/basics/_category_.json b/docusaurus/versioned_docs/version-1.2.0/basics/_category_.json new file mode 100644 index 000000000..fdad4c088 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/basics/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Basics", + "position": 2, + "link": { + "type": "generated-index", + "description": "Basic concepts of Odra Framework" + } +} diff --git a/docusaurus/versioned_docs/version-1.2.0/contract.png b/docusaurus/versioned_docs/version-1.2.0/contract.png new file mode 100644 index 000000000..f89e7939e Binary files /dev/null and b/docusaurus/versioned_docs/version-1.2.0/contract.png differ diff --git a/docusaurus/versioned_docs/version-1.2.0/docs-cover.png b/docusaurus/versioned_docs/version-1.2.0/docs-cover.png new file mode 100644 index 000000000..c43be0374 Binary files /dev/null and b/docusaurus/versioned_docs/version-1.2.0/docs-cover.png differ diff --git a/docusaurus/versioned_docs/version-1.2.0/examples/_category_.json b/docusaurus/versioned_docs/version-1.2.0/examples/_category_.json new file mode 100644 index 000000000..8806c6b29 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/examples/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Examples", + "position": 5, + "link": { + "type": "generated-index", + "description": "Examples" + } +} diff --git a/docusaurus/versioned_docs/version-1.2.0/examples/odra-examples.md b/docusaurus/versioned_docs/version-1.2.0/examples/odra-examples.md new file mode 100644 index 000000000..1230c17c0 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/examples/odra-examples.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 1 +--- + +# odra-examples +Odra repository provides rich source learning materials. We want to ensure you would feel comfortable with the framework from day one and make the learning curve as flat as possible. Are you a Solidity developer? Are you a Casper developer? Are you new to smart contracts development? To learn Odra from its creators, look at the `examples` in the [Odra main repository]. + +The examples we have prepared demonstrate in "real code" all the concepts you have read in this documentation, from a simple access control module ending up with a wallet where you can lock your native tokens for a certain amount of time. + +Don't worry if you find learning solely by reading the code challenging. Go to the [Tutorial](../category/tutorials/) section, where we will review it together. We will break the code into pieces, leaving no space for further questions. + +## What's next +Read the next article to learn about reusable Odra components encapsulated in `odra-modules`. + +[Odra main repository]: https://github.com/odradev/odra \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/examples/using-odra-modules.md b/docusaurus/versioned_docs/version-1.2.0/examples/using-odra-modules.md new file mode 100644 index 000000000..d10b624b2 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/examples/using-odra-modules.md @@ -0,0 +1,180 @@ +--- +sidebar_position: 2 +--- + +# Using odra-modules + +Besides the Odra framework, you can attach to your project `odra-module` - a set of plug-and-play modules. + +If you followed the [Installation guide] your Cargo.toml should look like: + +```toml title=Cargo.toml +[package] +name = "my_project" +version = "0.1.0" +edition = "2021" + +[dependencies] +odra = "1.1.0" + +[dev-dependencies] +odra-test = "1.1.0" + +[build-dependencies] +odra-build = "1.1.0" + +[[bin]] +name = "my_project_build_contract" +path = "bin/build_contract.rs" +test = false + +[[bin]] +name = "my_project_build_schema" +path = "bin/build_schema.rs" +test = false + +[profile.release] +codegen-units = 1 +lto = true + +[profile.dev.package."*"] +opt-level = 3 +``` + +To use `odra-modules`, edit your `dependency` and `features` sections. + +```toml title=Cargo.toml +[dependencies] +odra = "1.1.0" +odra-modules = "1.1.0" +``` + +Now, the only thing left is to add a module to your contract. + +Let's write an example of `MyToken` based on `Erc20` module. + +```rust +use odra::prelude::*; +use odra::{Address, casper_types::U256, module::SubModule}; +use odra_modules::erc20::Erc20; + +#[odra::module] +pub struct MyToken { + erc20: SubModule +} + +#[odra::module] +impl OwnedToken { + pub fn init(&mut self, initial_supply: U256) { + let name = String::from("MyToken"); + let symbol = String::from("MT"); + let decimals = 9u8; + self.erc20.init(name, symbol, decimals, initial_supply); + } + + pub fn name(&self) -> String { + self.erc20.name() + } + + pub fn symbol(&self) -> String { + self.erc20.symbol() + } + + pub fn decimals(&self) -> u8 { + self.erc20.decimals() + } + + pub fn total_supply(&self) -> U256 { + self.erc20.total_supply() + } + + pub fn balance_of(&self, address: Address) -> U256 { + self.erc20.balance_of(address) + } + + pub fn allowance(&self, owner: Address, spender: Address) -> U256 { + self.erc20.allowance(owner, spender) + } + + pub fn transfer(&mut self, recipient: Address, amount: U256) { + self.erc20.transfer(recipient, amount); + } + + pub fn transfer_from(&mut self, owner: Address, recipient: Address, amount: U256) { + self.erc20.transfer_from(owner, recipient, amount); + } + + pub fn approve(&mut self, spender: Address, amount: U256) { + self.erc20.approve(spender, amount); + } +} +``` + +:::info +All available modules are placed in the main [Odra repository]. +::: + +## Available modules + +Odra modules comes with couple of ready-to-use modules and reusable extensions. + +### Tokens + +#### CEP-18 +Casper Ecosystem Proposal 18 (CEP-18) is a standard interface for the CSPR and the custom made tokens. Inspired by the ERC20 standard. Read more about the CEP-18 [here](https://github.com/casper-network/ceps/blob/master/text/0018-token-standard.md). + +#### CEP-78 +Casper Ecosystem Proposal 78 (CEP-78) is an enhanced NFT standard focused on ease of use and installation. Inspired by the ERC721 standard. Read more about the CEP-78 [here](https://github.com/casper-network/ceps/blob/master/text/0078-enhanced-nft-standard.md). + +#### Erc20 + +The `Erc20` module implements the [ERC20](https://eips.ethereum.org/EIPS/eip-20) standard. + +#### Erc721 + +The `Erc721Base` module implements the [ERC721](https://eips.ethereum.org/EIPS/eip-721) standard, adjusted for the Odra framework. + +The `Erc721Token` module implements the `ERC721Base` and additionally uses +the `Erc721Metadata` and `Ownable` extensions. + +The `Erc721Receiver` trait lets you implement your own logic for receiving NFTs. + +The `OwnedErc721WithMetadata` trait is a combination of `Erc721Token`, `Erc721Metadata` and `Ownable` modules. + +#### Erc1155 + +The `Erc1155Base` module implements the [ERC1155](https://eips.ethereum.org/EIPS/eip-1155) standard, adjusted for the Odra framework. + +The `Erc1155Token` module implements the `ERC1155Base` and additionally uses the `Ownable` extension. + +The `OwnedErc1155` trait is a combination of `Erc1155Token` and `Ownable` modules. + +#### Wrapped native token + +The `WrappedNativeToken` module implements the Wrapper for the native token, +it was inspired by the WETH. + +### Access + +#### AccessControl +This module enables the implementation of role-based access control mechanisms for children +modules. Roles are identified by their 32-bytes identifier, which should be unique and exposed in the external API. + +#### Ownable +This module provides a straightforward access control feature that enables exclusive access to particular functions by an account, known as the owner. + +The account that initiates the module is automatically assigned as the owner. However, ownership can be transferred later by using the +`transfer_ownership()` function. + +#### Ownable2Step +An extension of the `Ownable` module. + +Ownership can be transferred in a two-step process by using `transfer_ownership()` and `accept_ownership()` functions. + +### Security + +#### Pausable +A module allowing to implement an emergency stop mechanism that can be triggered by any account. + +[Installation guide]: ../getting-started/installation.md +[Odra repository]: https://github.com/odradev/odra \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/getting-started/_category_.json b/docusaurus/versioned_docs/version-1.2.0/getting-started/_category_.json new file mode 100644 index 000000000..b8194d65d --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/getting-started/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Getting started", + "position": 1, + "link": { + "type": "generated-index", + "description": "5 minutes to learn the most important Odra concepts." + } +} diff --git a/docusaurus/versioned_docs/version-1.2.0/getting-started/flipper.md b/docusaurus/versioned_docs/version-1.2.0/getting-started/flipper.md new file mode 100644 index 000000000..ed645126a --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/getting-started/flipper.md @@ -0,0 +1,95 @@ +--- +sidebar_position: 2 +--- + +# Flipper example + +To quickly start working with Odra, take a look at the following code sample. If you followed the +[Installation](installation.md) tutorial, you should have this file already at `src/flipper.rs`. + +For further explanation of how this code works, see [Flipper Internals](../basics/04-flipper-internals.md). + +## Let's flip + +```rust title="flipper.rs" showLineNumbers +use odra::Var; + +/// A module definition. Each module struct consists Vars and Mappings +/// or/and another modules. +#[odra::module] +pub struct Flipper { + /// The module itself does not store the value, + /// it's a proxy that writes/reads value to/from the host. + value: Var, +} + +/// Module implementation. +/// +/// To generate entrypoints, +/// an implementation block must be marked as #[odra::module]. +#[odra::module] +impl Flipper { + /// Odra constructor, must be named `init`. + /// + /// Initializes the contract with the value of value. + pub fn init(&mut self) { + self.value.set(false); + } + + /// Replaces the current value with the passed argument. + pub fn set(&mut self, value: bool) { + self.value.set(value); + } + + /// Replaces the current value with the opposite value. + pub fn flip(&mut self) { + self.value.set(!self.get()); + } + + /// Retrieves value from the storage. + /// If the value has never been set, the default value is returned. + pub fn get(&self) -> bool { + self.value.get_or_default() + } +} + +#[cfg(test)] +mod tests { + use crate::flipper::FlipperHostRef; + use odra::host::{Deployer, NoArgs}; + + #[test] + fn flipping() { + let env = odra_test::env(); + // To test a module we need to deploy it. Autogenerated `FlipperHostRef` + // implements `Deployer` trait, so we can use it to deploy the module. + let mut contract = FlipperHostRef::deploy(&env, NoArgs); + assert!(!contract.get()); + contract.flip(); + assert!(contract.get()); + } + + #[test] + fn test_two_flippers() { + let env = odra_test::env(); + let mut contract1 = FlipperHostRef::deploy(&env, NoArgs); + let contract2 = FlipperHostRef::deploy(&env, NoArgs); + assert!(!contract1.get()); + assert!(!contract2.get()); + contract1.flip(); + assert!(contract1.get()); + assert!(!contract2.get()); + } +} +``` + +## Testing + +To run the tests, execute the following command: + +```bash +cargo odra test # or add the `-b casper` flag to run tests on the CasperVM +``` + +## What's next +In the next category of articles, we will go through basic concepts of Odra. \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/getting-started/installation.md b/docusaurus/versioned_docs/version-1.2.0/getting-started/installation.md new file mode 100644 index 000000000..2b29cd0de --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/getting-started/installation.md @@ -0,0 +1,73 @@ +--- +sidebar_position: 1 +--- + +# Installation + +Hello fellow Odra user! This page will guide you through the installation process. + +## Prerequisites +To start working with Odra, you need to have the following installed on your machine: + +- Rust toolchain installed (see [rustup.rs](https://rustup.rs/)) +- wasmstrip tool installed (see [wabt](https://github.com/WebAssembly/wabt)) + +We do not provide exact commands for installing these tools, as they are different for different operating systems. +Please refer to the documentation of the tools themselves. + +With Rust toolchain ready, you can add a new target: + +```bash +rustup target add wasm32-unknown-unknown +``` + +:::note +`wasm32-unknown-unknown` is a target that will be used by Odra to compile your smart contracts to WASM files. +::: + +## Installing Cargo Odra + +Cargo Odra is a helpful tool that will help you to build and test your smart contracts. +It is not required to use Odra, but the documentation will assume that you have it installed. + +To install it, simply execute the following command: + +```bash +cargo install cargo-odra --locked +``` + +To check if it was installed correctly and see available commands, type: + +```bash +cargo odra --help +``` + +If everything went fine, we can proceed to the next step. + +## Creating a new Odra project + +To create a new project, simply execute: + +```bash +cargo odra new --name my-project && cd my_project +``` + +This will create a new folder called `my_project` and initialize Odra there. Cargo Odra +will create a sample contract for you in `src` directory. You can run the tests of this contract +by executing: + +```bash +cargo odra test +``` + +This will run tests using Odra's internal OdraVM. You can run those tests against a real backend, let's use CasperVM: + +```bash +cargo odra test -b casper +``` + +**Congratulations!** Now you are ready to create contracts using Odra framework! If you had any problems during +the installation process, feel free to ask for help on our [Discord](https://discord.com/invite/Mm5ABc9P8k). + +## What's next? +If you want to see the code that you just tested, continue to the description of [Flipper example](flipper). diff --git a/docusaurus/versioned_docs/version-1.2.0/intro.md b/docusaurus/versioned_docs/version-1.2.0/intro.md new file mode 100644 index 000000000..b82458d62 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/intro.md @@ -0,0 +1,22 @@ +--- +sidebar_position: 0 +slug: / +image: "./docs-cover.png" +description: Odra Docs +--- + +# Odra framework + +Odra is a Rust-based smart contract framework for [Casper Network]. Odra encourages rapid development and clean, +pragmatic design. Built by experienced developers, takes care of much of the hassle of smart contract +development, enabling you to focus on writing your dapp without reinventing the wheel. + +It's free and open source! + +## What's next + +See the [Installation] and our [Flipper example] to find out how to start your new project with Odra. + +[Casper Network]: https://casper.network +[Installation]: getting-started/installation.md +[Flipper example]: getting-started/flipper.md \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/migrations/_category_.json b/docusaurus/versioned_docs/version-1.2.0/migrations/_category_.json new file mode 100644 index 000000000..98729aec0 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/migrations/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Migrations", + "position": 7, + "link": { + "type": "generated-index", + "description": "How to keep your code in sync with the latest version of the Odra Framework." + } +} diff --git a/docusaurus/versioned_docs/version-1.2.0/migrations/to-0.8.0.md b/docusaurus/versioned_docs/version-1.2.0/migrations/to-0.8.0.md new file mode 100644 index 000000000..78139a904 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/migrations/to-0.8.0.md @@ -0,0 +1,1093 @@ +--- +sidebar_position: 1 +description: Migration guide to v0.8.0 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Migration guide to v0.8.0 + +Odra v0.8.0 introduces several breaking changes that require users to update their smart contracts and tests. This migration guide provides a detailed overview of the changes, along with step-by-step instructions for migrating existing code to the new version. + +This guide is intended for developers who have built smart contracts using previous versions of Odra and need to update their code to be compatible with v0.8.0. It assumes a basic understanding of smart contract development and the Odra framework. If you're new to Odra, we recommend to start your journey with the [Getting Started](../category/getting-started/). + +The most significant changes in v0.8.0 include: +- Odra is not a blockchain-agnostic framework anymore. It is now a Casper smart contract framework only. +- Framework internals redesign. + +## **1. Prerequisites** + +### 1.1. **Update cargo-odra** +Before you begin the migration process, make sure you installed the latest version of the Cargo Odra toolchain. You can install it by running the following command: + +```bash +cargo install cargo-odra --force --locked +``` + +### 1.2. **Review the Changelog** +Before you move to changing your code, start by reviewing the [Changelog] to understand the changes introduced in v0.8.0. + + +## **2. Migration Steps** + +### 2.1 **Add bin directory** +Odra 0.8.0 introduces a new way to build smart contracts. The `.builder_casper` directory is no longer used. Instead, you should create a new directory called `bin` in the root of your project and add the `build_contract.rs` and `build_schema.rs` files to the `bin` directory. + +You can find the `build_contract.rs` and `build_schema.rs` files in [templates] directory in the Odra main repository. You can choose whatever template you want to use and copy the files to your project. In both files, you should replace `{{project-name}}` with the name of your project. + + +### 2.2. **Update Cargo.toml** +There a bunch of changes in the `Cargo.toml` file. +* You don't have to specify the features anymore - remove the `features` section and `default-features` flag from the `odra` dependency. +* Register bins you added in the previous step. +* Add `dev-dependencies` section with `odra-test` crate. +* Add recommended profiles for `release` and `dev` to optimize the build process. + +Below you can compare the `Cargo.toml` file after and before the migration to v0.8.0: + + + + +```toml +[package] +name = "my_project" +version = "0.1.0" +edition = "2021" + +[dependencies] +odra = "0.8.0" + +[dev-dependencies] +odra-test = "0.8.0" + +[[bin]] +name = "my_project_build_contract" +path = "bin/build_contract.rs" +test = false + +[[bin]] +name = "my_project_build_schema" +path = "bin/build_schema.rs" +test = false + +[profile.release] +codegen-units = 1 +lto = true + +[profile.dev.package."*"] +opt-level = 3 + +``` + + + +```toml +[package] +name = "my_project" +version = "0.1.0" +edition = "2021" + +[dependencies] +odra = { version = "0.7.1", default-features = false } + +[features] +default = ["mock-vm"] +mock-vm = ["odra/mock-vm"] +casper = ["odra/casper"] +``` + + + + + +### 2.2. **Update Odra.toml** +Due to the changes in cargo-odra, the `Odra.toml` file has been simplified. The `name` property is no longer required. + + + + +```toml +[[contracts]] +fqn = "my_project::Flipper" +``` + + + +```toml +[[contracts]] +name = "flipper" +fqn = "my_project::Flipper" +``` + + + + + +### 2.3. **Update Smart Contracts** + +The smart contracts themselves will need to be updated to work with the new version of the framework. The changes will depend on the specific features and APIs used in the contracts. Here are some common changes you might need to make: + +#### 2.3.1. **Update the `use` statements to reflect the new module structure.** + * Big integer types are now located in the `odra::casper_types` module. + * `odra::types::Address` is now `odra::Address`. + * `Variable` is now `Var`. + * Remove `odra::contract_env`. + * Remove `odra::types::event::OdraEvent`. + * Remove `odra::types::OdraType` as it is no longer required. + * Change `odra::types::casper_types::*;` to `odra::casper_types::*;`. + +#### 2.3.2. **Some type aliases are no longer in use.** + * `Balance` - use `odra::casper_types::U512`. + * `BlockTime` - use `u64`. + * `EventData` - use `odra::casper_types::bytesrepr::Bytes`. + +#### 2.3.3. **Consider import `odra::prelude::*` in your module files.** + +#### 2.3.4. **Flatten nested `Mapping`s.** +```rust +// Before +#[odra::module(events = [Approval, Transfer])] +pub struct Erc20 { + ... + allowances: Mapping> +} +// After +#[odra::module(events = [Approval, Transfer])] +pub struct Erc20 { + ... + allowances: Mapping<(Address, Address), U256> +} +``` +#### 2.3.5. **Update errors definitions.** + +`execution_error!` macro has been replace with `OdraError` derive macro. + + + + +```rust +use odra::OdraError; + +#[derive(OdraError)] +pub enum Error { + InsufficientBalance = 30_000, + InsufficientAllowance = 30_001, + NameNotSet = 30_002, + SymbolNotSet = 30_003, + DecimalsNotSet = 30_004 +} +``` + + + +```rust +use odra::execution_error; + +execution_error! { + pub enum Error { + InsufficientBalance => 30_000, + InsufficientAllowance => 30_001, + NameNotSet => 30_002, + SymbolNotSet => 30_003, + DecimalsNotSet => 30_004, + } +} +``` + + + + +#### 2.3.6. **Update events definitions.** + + + + +```rust +use odra::prelude::*; +use odra::Event; + +#[derive(Event, Eq, PartialEq, Debug)] +pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 +} + +// Emitting the event +self.env().emit_event(Transfer { + from: None, + to: Some(*address), + amount: *amount +}); +``` + + + +```rust +use odra::Event; + +#[derive(Event, Eq, PartialEq, Debug)] +pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 +} + +// Emitting the event +use odra::types::event::OdraEvent; + +Transfer { + from: Some(*owner), + to: Some(*recipient), + amount: *amount +}.emit(); +``` + + + + +#### 2.3.7. **Replace `contract_env` with `self.env()` in your modules.** + +`self.env()` is a new way to access the contract environment, returns a reference to `ContractEnv`. The API is similar to the previous `contract_env` but with some changes. +* `fn get_var(key: &[u8]) -> Option` is now `fn get_value(&self, key: &[u8]) -> Option`. +* `fn set_var(key: &[u8], value: T)` is now `fn set_value(&self, key: &[u8], value: T)`. +* `set_dict_value()` and `get_dict_value()` has been removed. All the dictionary operations should be performed using `Mapping` type, internally using `set_var()` and `get_var()` functions. +* `fn hash>(input: T) -> Vec` is now `fn hash(&self, value: T) -> [u8; 32]`. +* `fn revert>(error: E) -> !` is now `fn revert>(&self, error: E) -> !`. +* `fn emit_event(event: T)` is now `fn emit_event(&self, event: T)`. +* `fn call_contract(address: Address, entrypoint: &str, args: &RuntimeArgs, amount: Option) -> T` is now `fn call_contract(&self, address: Address, call: CallDef) -> T`. +* functions `native_token_metadata()` and `one_token()` have been removed. + +#### 2.3.8. **Wrap submodules of your module with `odra::SubModule`.** + + + + +```rust +#[odra::module(events = [Transfer])] +pub struct Erc721Token { + core: SubModule, + metadata: SubModule, + ownable: SubModule +} +``` + + + +```rust +#[odra::module(events = [Transfer])] +pub struct Erc721Token { + core: Erc721Base, + metadata: Erc721MetadataExtension, + ownable: Ownable +} +``` + + + + +#### 2.3.9. **Update external contract calls.** + +However the definition of an external contract remains the same, the way you call it has changed. A reference to an external contract is named `{{ModuleName}}ContractRef` (former `{{ModuleName}}Ref`) and you can call it using `{{ModuleName}}ContractRef::new(env, address)` (former `{{ModuleName}}Ref::at()`). + + + + +```rust +#[odra::external_contract] +pub trait Token { + fn balance_of(&self, owner: &Address) -> U256; +} + +// Usage +TokenContractRef::new(env, token).balance_of(account) +``` + + + +```rust +#[odra::external_contract] +pub trait Token { + fn balance_of(&self, owner: &Address) -> U256; +} + +// Usage +TokenRef::at(token).balance_of(account) +``` + + + + +#### 2.3.10. **Update constructors.** + +Remove the `#[odra::init]` attribute from the constructor and ensure that the constructor function is named `init`. + +#### 2.3.11. **Update `UnwrapOrRevert` calls.** + +The functions `unwrap_or_revert` and `unwrap_or_revert_with` now require `&HostEnv` as the first parameter. + +#### 2.3.12. **Remove `#[odra(using)]` attribute from your module definition.** + +Sharing the same instance of a module is no longer supported. A redesign of the module structure might be required. + +### 2.4. **Update Tests** + +Once you've updated your smart contracts, you'll need to update your tests to reflect the changes. The changes will depend on the specific features and APIs used in the tests. Here are some common changes you might need to make: + +#### 2.4.1. **Contract deployment.** + +The way you deploy a contract has changed: + +1. You should use `{{ModuleName}}HostRef::deploy(&env, args)` instead of `{{ModuleName}}Deployer::init()`. The `{{ModuleName}}HostRef` implements `odra::host::Deployer`. +2. Instantiate the `HostEnv` using `odra_test::env()`, required by the `odra::host::Deployer::deploy()` function. +3. If the contract doesn't have init args, you should use `odra::host::NoArgs` as the second argument of the `deploy` function. +4. If the contract has init args, you should pass the autogenerated `{{ModuleName}}InitArgs` as the second argument of the `deploy` function. + + + + +```rust +// A contract without init args +use super::OwnableHostRef; +use odra::host::{Deployer, HostEnv, HostRef, NoArgs}; + +let env: HostEnv = odra_test::env(); +let ownable = OwnableHostRef::deploy(&env, NoArgs) + +// A contract with init args +use super::{Erc20HostRef, Erc20InitArgs}; +use odra::host::{Deployer, HostEnv}; + +let env: HostEnv = odra_test::env(); +let init_args = Erc20InitArgs { + symbol: SYMBOL.to_string(), + name: NAME.to_string(), + decimals: DECIMALS, + initial_supply: Some(INITIAL_SUPPLY.into()) +}; +let erc20 = Erc20HostRef::deploy(&env, init_args); +``` + + + +```rust +// A contract without init args +use super::OwnableDeployer; + +let ownable = OwnableDeployer::init(); + +// A contract with init args +let erc20 = Erc20Deployer::init( + SYMBOL.to_string(), + NAME.to_string(), + DECIMALS, + &Some(INITIAL_SUPPLY.into()) +); +``` + + + + +#### 2.4.2. **Host interactions.** + + +1. Replace `odra::test_env` with `odra_test::env()`. +2. The API of `odra::test_env` and `odra_test::env()` are similar, but there are some differences: + * `test_env::advance_block_time_by(BlockTime)` is now `env.advance_block_time(u64)`. + * `test_env::token_balance(Address)` is now `env.balance_of(&Address)`. + * functions `test_env::last_call_contract_gas_cost()`, `test_env::last_call_contract_gas_used()`, `test_env::total_gas_used(Address)`, `test_env::gas_report()` have been removed. You should use `HostRef::last_call()` and extract the data from a `odra::ContractCallResult` instance. `HostRef` is a trait implemented by `{{ModuleName}}HostRef`. + +#### 2.4.3. **Testing failing scenarios.** + +`test_env::assert_exception()` has been removed. You should use the `try_` prefix to call the function and then assert the result. +`try_` prefix is a new way to call a function that might fail. It returns a [`OdraResult`] type, which you can then assert using the standard Rust `assert_eq!` macro. + + + + +```rust +#[test] +fn transfer_from_error() { + let (env, mut erc20) = setup(); + + let (owner, spender, recipient) = + (env.get_account(0), env.get_account(1), env.get_account(2)); + let amount = 1_000.into(); + env.set_caller(spender); + + assert_eq!( + erc20.try_transfer_from(owner, recipient, amount), + Err(Error::InsufficientAllowance.into()) + ); +} +``` + + + +```rust +#[test] +fn transfer_from_error() { + test_env::assert_exception(Error::InsufficientAllowance, || { + let mut erc20 = setup(); + + let (owner, spender, recipient) = ( + test_env::get_account(0), + test_env::get_account(1), + test_env::get_account(2) + ); + let amount = 1_000.into(); + test_env::set_caller(spender); + + erc20.transfer_from(&owner, &recipient, &amount) + }); +} +``` + + + + +#### 2.4.4. **Testing events.** + +`assert_events!` macro has been removed. You should use `HostEnv::emitted_event()` to assert the emitted events. +The new API doesn't allow to assert multiple events at once, but adds alternative ways to assert the emitted events. Check the [`HostEnv`] documentation to explore the available options. + + + + +```rust +let env: HostEnv = odra_test::env(); +let erc20 = Erc20HostRef::deploy(&env, init_args); + +... + +assert!(env.emitted_event( + erc20.address(), + &Approval { + owner, + spender, + value: approved_amount - transfer_amount + } +)); +assert!(env.emitted_event( + erc20.address(), + &Transfer { + from: Some(owner), + to: Some(recipient), + amount: transfer_amount + } +)); +``` + + + +```rust +let erc20 = Erc20HostDeployer::init(&env, ...); + +... + +assert_events!( + erc20, + Approval { + owner, + spender, + value: approved_amount - transfer_amount + }, + Transfer { + from: Some(owner), + to: Some(recipient), + amount: transfer_amount + } +); +``` + + + + +## 3. **Code Examples** + +Here is a complete example of a smart contract after and before the migration to v0.8.0. + + + + +```rust title="src/erc20.rs" +use crate::erc20::errors::Error::*; +use crate::erc20::events::*; +use odra::prelude::*; +use odra::{casper_types::U256, Address, Mapping, Var}; + +#[odra::module(events = [Approval, Transfer])] +pub struct Erc20 { + decimals: Var, + symbol: Var, + name: Var, + total_supply: Var, + balances: Mapping, + allowances: Mapping<(Address, Address), U256> +} + +#[odra::module] +impl Erc20 { + pub fn init( + &mut self, + symbol: String, + name: String, + decimals: u8, + initial_supply: Option + ) { + let caller = self.env().caller(); + self.symbol.set(symbol); + self.name.set(name); + self.decimals.set(decimals); + + if let Some(initial_supply) = initial_supply { + self.total_supply.set(initial_supply); + self.balances.set(&caller, initial_supply); + + if !initial_supply.is_zero() { + self.env().emit_event(Transfer { + from: None, + to: Some(caller), + amount: initial_supply + }); + } + } + } + + pub fn transfer(&mut self, recipient: &Address, amount: &U256) { + let caller = self.env().caller(); + self.raw_transfer(&caller, recipient, amount); + } + + pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + let spender = self.env().caller(); + + self.spend_allowance(owner, &spender, amount); + self.raw_transfer(owner, recipient, amount); + } + + pub fn approve(&mut self, spender: &Address, amount: &U256) { + let owner = self.env().caller(); + + self.allowances.set(&(owner, *spender), *amount); + self.env().emit_event(Approval { + owner, + spender: *spender, + value: *amount + }); + } + + pub fn name(&self) -> String { + self.name.get_or_revert_with(NameNotSet) + } + + // Other getter functions... + + pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 { + self.allowances.get_or_default(&(*owner, *spender)) + } + + pub fn mint(&mut self, address: &Address, amount: &U256) { + self.total_supply.add(*amount); + self.balances.add(address, *amount); + + self.env().emit_event(Transfer { + from: None, + to: Some(*address), + amount: *amount + }); + } + + pub fn burn(&mut self, address: &Address, amount: &U256) { + if self.balance_of(address) < *amount { + self.env().revert(InsufficientBalance); + } + self.total_supply.subtract(*amount); + self.balances.subtract(address, *amount); + + self.env().emit_event(Transfer { + from: Some(*address), + to: None, + amount: *amount + }); + } +} + +impl Erc20 { + fn raw_transfer(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + if *amount > self.balances.get_or_default(owner) { + self.env().revert(InsufficientBalance) + } + + self.balances.subtract(owner, *amount); + self.balances.add(recipient, *amount); + + self.env().emit_event(Transfer { + from: Some(*owner), + to: Some(*recipient), + amount: *amount + }); + } + + fn spend_allowance(&mut self, owner: &Address, spender: &Address, amount: &U256) { + let allowance = self.allowances.get_or_default(&(*owner, *spender)); + if allowance < *amount { + self.env().revert(InsufficientAllowance) + } + self.allowances.subtract(&(*owner, *spender), *amount); + + self.env().emit_event(Approval { + owner: *owner, + spender: *spender, + value: allowance - *amount + }); + } +} + +pub mod events { + use odra::prelude::*; + use odra::{casper_types::U256, Address, Event}; + + #[derive(Event, Eq, PartialEq, Debug)] + pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 + } + + #[derive(Event, Eq, PartialEq, Debug)] + pub struct Approval { + pub owner: Address, + pub spender: Address, + pub value: U256 + } +} + +pub mod errors { + use odra::OdraError; + + #[derive(OdraError)] + pub enum Error { + InsufficientBalance = 30_000, + InsufficientAllowance = 30_001, + NameNotSet = 30_002, + SymbolNotSet = 30_003, + DecimalsNotSet = 30_004 + } +} + +#[cfg(test)] +mod tests { + use super::{ + errors::Error, + events::{Approval, Transfer}, + Erc20HostRef, Erc20InitArgs + }; + use odra::{ + casper_types::U256, + host::{Deployer, HostEnv, HostRef}, + prelude::* + }; + + const NAME: &str = "Plascoin"; + const SYMBOL: &str = "PLS"; + const DECIMALS: u8 = 10; + const INITIAL_SUPPLY: u32 = 10_000; + + fn setup() -> (HostEnv, Erc20HostRef) { + let env = odra_test::env(); + ( + env.clone(), + Erc20HostRef::deploy( + &env, + Erc20InitArgs { + symbol: SYMBOL.to_string(), + name: NAME.to_string(), + decimals: DECIMALS, + initial_supply: Some(INITIAL_SUPPLY.into()) + } + ) + ) + } + + #[test] + fn initialization() { + // When deploy a contract with the initial supply. + let (env, erc20) = setup(); + + // Then the contract has the metadata set. + assert_eq!(erc20.symbol(), SYMBOL.to_string()); + assert_eq!(erc20.name(), NAME.to_string()); + assert_eq!(erc20.decimals(), DECIMALS); + + // Then the total supply is updated. + assert_eq!(erc20.total_supply(), INITIAL_SUPPLY.into()); + + // Then a Transfer event was emitted. + assert!(env.emitted_event( + erc20.address(), + &Transfer { + from: None, + to: Some(env.get_account(0)), + amount: INITIAL_SUPPLY.into() + } + )); + } + + #[test] + fn transfer_works() { + // Given a new contract. + let (env, mut erc20) = setup(); + + // When transfer tokens to a recipient. + let sender = env.get_account(0); + let recipient = env.get_account(1); + let amount = 1_000.into(); + erc20.transfer(&recipient, &amount); + + // Then the sender balance is deducted. + assert_eq!( + erc20.balance_of(&sender), + U256::from(INITIAL_SUPPLY) - amount + ); + + // Then the recipient balance is updated. + assert_eq!(erc20.balance_of(&recipient), amount); + + // Then Transfer event was emitted. + assert!(env.emitted_event( + erc20.address(), + &Transfer { + from: Some(sender), + to: Some(recipient), + amount + } + )); + } + + #[test] + fn transfer_error() { + // Given a new contract. + let (env, mut erc20) = setup(); + + // When the transfer amount exceeds the sender balance. + let recipient = env.get_account(1); + let amount = U256::from(INITIAL_SUPPLY) + U256::one(); + + // Then an error occurs. + assert!(erc20.try_transfer(&recipient, &amount).is_err()); + } + + // Other tests... +} +``` + + + +```rust title="src/erc20.rs" +use odra::prelude::string::String; +use odra::{ + contract_env, + types::{event::OdraEvent, Address, U256}, + Mapping, UnwrapOrRevert, Variable +}; + +use self::{ + errors::Error, + events::{Approval, Transfer} +}; + +#[odra::module(events = [Approval, Transfer])] +pub struct Erc20 { + decimals: Variable, + symbol: Variable, + name: Variable, + total_supply: Variable, + balances: Mapping, + allowances: Mapping> +} + +#[odra::module] +impl Erc20 { + #[odra(init)] + pub fn init( + &mut self, + symbol: String, + name: String, + decimals: u8, + initial_supply: &Option + ) { + let caller = contract_env::caller(); + + self.symbol.set(symbol); + self.name.set(name); + self.decimals.set(decimals); + + if let Some(initial_supply) = *initial_supply { + self.total_supply.set(initial_supply); + self.balances.set(&caller, initial_supply); + + if !initial_supply.is_zero() { + Transfer { + from: None, + to: Some(caller), + amount: initial_supply + } + .emit(); + } + } + } + + pub fn transfer(&mut self, recipient: &Address, amount: &U256) { + let caller = contract_env::caller(); + self.raw_transfer(&caller, recipient, amount); + } + + pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + let spender = contract_env::caller(); + + self.spend_allowance(owner, &spender, amount); + self.raw_transfer(owner, recipient, amount); + } + + pub fn approve(&mut self, spender: &Address, amount: &U256) { + let owner = contract_env::caller(); + + self.allowances.get_instance(&owner).set(spender, *amount); + Approval { + owner, + spender: *spender, + value: *amount + } + .emit(); + } + + pub fn name(&self) -> String { + self.name.get().unwrap_or_revert_with(Error::NameNotSet) + } + + // Other getter functions... + + pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 { + self.allowances.get_instance(owner).get_or_default(spender) + } + + pub fn mint(&mut self, address: &Address, amount: &U256) { + self.total_supply.add(*amount); + self.balances.add(address, *amount); + + Transfer { + from: None, + to: Some(*address), + amount: *amount + } + .emit(); + } + + pub fn burn(&mut self, address: &Address, amount: &U256) { + if self.balance_of(address) < *amount { + contract_env::revert(Error::InsufficientBalance); + } + self.total_supply.subtract(*amount); + self.balances.subtract(address, *amount); + + Transfer { + from: Some(*address), + to: None, + amount: *amount + } + .emit(); + } +} + +impl Erc20 { + fn raw_transfer(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + if *amount > self.balances.get_or_default(owner) { + contract_env::revert(Error::InsufficientBalance) + } + + self.balances.subtract(owner, *amount); + self.balances.add(recipient, *amount); + + Transfer { + from: Some(*owner), + to: Some(*recipient), + amount: *amount + } + .emit(); + } + + fn spend_allowance(&mut self, owner: &Address, spender: &Address, amount: &U256) { + let allowance = self.allowances.get_instance(owner).get_or_default(spender); + if allowance < *amount { + contract_env::revert(Error::InsufficientAllowance) + } + self.allowances + .get_instance(owner) + .subtract(spender, *amount); + Approval { + owner: *owner, + spender: *spender, + value: allowance - *amount + } + .emit(); + } +} + +pub mod events { + use odra::types::{casper_types::U256, Address}; + use odra::Event; + + #[derive(Event, Eq, PartialEq, Debug)] + pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 + } + + #[derive(Event, Eq, PartialEq, Debug)] + pub struct Approval { + pub owner: Address, + pub spender: Address, + pub value: U256 + } +} + +pub mod errors { + use odra::execution_error; + + execution_error! { + pub enum Error { + InsufficientBalance => 30_000, + InsufficientAllowance => 30_001, + NameNotSet => 30_002, + SymbolNotSet => 30_003, + DecimalsNotSet => 30_004, + } + } +} + +#[cfg(test)] +mod tests { + use super::{ + errors::Error, + events::{Approval, Transfer}, + Erc20Deployer, Erc20Ref + }; + use odra::prelude::string::ToString; + use odra::{assert_events, test_env, types::casper_types::U256}; + + const NAME: &str = "Plascoin"; + const SYMBOL: &str = "PLS"; + const DECIMALS: u8 = 10; + const INITIAL_SUPPLY: u32 = 10_000; + + fn setup() -> Erc20Ref { + Erc20Deployer::init( + SYMBOL.to_string(), + NAME.to_string(), + DECIMALS, + &Some(INITIAL_SUPPLY.into()) + ) + } + + #[test] + fn initialization() { + // When deploy a contract with the initial supply. + let erc20 = setup(); + + // Then the contract has the metadata set. + assert_eq!(erc20.symbol(), SYMBOL.to_string()); + assert_eq!(erc20.name(), NAME.to_string()); + assert_eq!(erc20.decimals(), DECIMALS); + + // Then the total supply is updated. + assert_eq!(erc20.total_supply(), INITIAL_SUPPLY.into()); + + // Then a Transfer event was emitted. + assert_events!( + erc20, + Transfer { + from: None, + to: Some(test_env::get_account(0)), + amount: INITIAL_SUPPLY.into() + } + ); + } + + #[test] + fn transfer_works() { + // Given a new contract. + let mut erc20 = setup(); + + // When transfer tokens to a recipient. + let sender = test_env::get_account(0); + let recipient = test_env::get_account(1); + let amount = 1_000.into(); + erc20.transfer(&recipient, &amount); + + // Then the sender balance is deducted. + assert_eq!( + erc20.balance_of(&sender), + U256::from(INITIAL_SUPPLY) - amount + ); + + // Then the recipient balance is updated. + assert_eq!(erc20.balance_of(&recipient), amount); + + // Then Transfer event was emitted. + assert_events!( + erc20, + Transfer { + from: Some(sender), + to: Some(recipient), + amount + } + ); + } + + #[test] + fn transfer_error() { + test_env::assert_exception(Error::InsufficientBalance, || { + // Given a new contract. + let mut erc20 = setup(); + + // When the transfer amount exceeds the sender balance. + let recipient = test_env::get_account(1); + let amount = U256::from(INITIAL_SUPPLY) + U256::one(); + + // Then an error occurs. + erc20.transfer(&recipient, &amount) + }); + } + + // Other tests... +} +``` + + + + +## 4. **Troubleshooting** + +If you encounter any further issues after completing the migration steps, please don't hesitate to reach out to us on [Discord] or explore the other sections this documentation. You can also refer to the [technical documentation] for more detailed information. Additionally, our [examples] repository offers a wide range of examples to assist you in understanding the new features and APIs. Be sure to carefully review any compilation errors and warnings, as they may provide valuable insights into the necessary adjustments. + + +## 5. **References** + - [Changelog] + - [Odra Documentation] + - [Docs.rs] + - [Examples] + +[Changelog]: https://github.com/odradev/odra/blob/release/0.8.0/CHANGELOG.md +[templates]: https://github.com/odradev/odra/blob/release/0.8.0/templates +[`HostEnv`]: https://docs.rs/odra/0.8.0/odra/host/struct.HostEnv.html +[`OdraResult`]: https://docs.rs/odra/0.8.0/odra/type.OdraResult.html +[Discord]: https://discord.com/invite/Mm5ABc9P8k +[Odra Documentation]: https://docs.odra.dev +[technical documentation]: https://docs.rs/odra/0.8.0/odra/index.html +[Docs.rs]: https://docs.rs/odra/0.8.0/odra/index.html +[examples]: https:://github.com/odradev/odra/tree/release/0.8.0/examples +[Examples]: https:://github.com/odradev/odra/tree/release/0.8.0/examples + diff --git a/docusaurus/versioned_docs/version-1.2.0/migrations/to-0.9.0.md b/docusaurus/versioned_docs/version-1.2.0/migrations/to-0.9.0.md new file mode 100644 index 000000000..7180cd91f --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/migrations/to-0.9.0.md @@ -0,0 +1,529 @@ +--- +sidebar_position: 2 +description: Migration guide to v0.9.0 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Migration guide to v0.9.0 + +This guide is intended for developers who have built smart contracts using version 0.8.0 of Odra and need to update their code to be compatible with v0.9.0. For migration from version `0.7.1` and below, start with the [previous guide]. It assumes a basic understanding of smart contract development and the Odra framework. If you're new to Odra, we recommend to start your journey with the [Getting Started](../category/getting-started/). + +The most significant change in `0.9.0` is the way of defining custom elements namely type, events and errors. + +## **1. Prerequisites** + +### 1.1. **Update cargo-odra** +Before you begin the migration process, make sure you installed the latest version of the Cargo Odra toolchain. You can install it by running the following command: + +```bash +cargo install cargo-odra --force --locked +``` + +### 1.2. **Review the Changelog** +Before you move to changing your code, start by reviewing the [Changelog] to understand the changes introduced in v0.9.0. + +## **2. Migration Steps** + +### 2.1 **Update build_schema.rs bin** +Odra 0.9.0 adds a new standardized way of generating contract schema - [Casper Contract Schema]. You can find the updated `build_schema.rs` file in [templates] directory in the Odra main repository. You can choose whatever template you want to use and copy the files to your project. In both files, you should replace `{{project-name}}` with the name of your project. + +### 2.2 **Update smart contract code** + +The main changes in the smart contract code are related to the way of defining custom types, events and errors. The following sections will guide you through the necessary changes. + +#### 2.2.1. **Update custom types definitions.** + +`#[derive(OdraType)]` attribute has been replace with `#[odra::odra_type]` attribute. + + + + +```rust +use odra::Address; + +#[odra::odra_type] +pub struct Dog { + pub name: String, + pub age: u8, + pub owner: Option
+} +``` + + + +```rust +use odra::{Address, OdraType}; + +#[derive(OdraType)] +pub struct Dog { + pub name: String, + pub age: u8, + pub owner: Option
+} +``` + + + + +#### 2.2.2. **Update errors definitions.** + +`#[derive(OdraError)]` attribute has been replace with `#[odra::odra_error]` attribute. +Error enum should be passed as a parameter to the `#[odra::module]` attribute. + + + + +```rust +#[odra::module(events = [/* events go here */], errors = Error)] +pub struct Erc20 { + // fields +} + +#[odra::odra_error] +pub enum Error { + InsufficientBalance = 30_000, + InsufficientAllowance = 30_001, + NameNotSet = 30_002, + SymbolNotSet = 30_003, + DecimalsNotSet = 30_004 +} +``` + + + +```rust +#[odra::module(events = [/* events go here */])] +pub struct Erc20 { + // fields +} + +use odra::OdraError; + +#[derive(OdraError)] +pub enum Error { + InsufficientBalance = 30_000, + InsufficientAllowance = 30_001, + NameNotSet = 30_002, + SymbolNotSet = 30_003, + DecimalsNotSet = 30_004 +} +``` + + + + +#### 2.2.3. **Update events definitions.** + +`#[derive(Event)]` attribute has been replace with `#[odra::event]` attribute. + + + + +```rust +use odra::prelude::*; +use odra::{Address, casper_types::U256}; + +#[odra::event] +pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 +} +``` + + + +```rust +use odra::prelude::*; +use odra::{Address, casper_types::U256, Event}; + +#[derive(Event, Eq, PartialEq, Debug)] +pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 +} +``` + + + + +## 3. **Code Examples** + +Here is a complete example of a smart contract after and before the migration to v0.9.0. + + + + +```rust title="src/erc20.rs" +use crate::erc20::errors::Error; +use crate::erc20::events::*; +use odra::prelude::*; +use odra::{casper_types::U256, Address, Mapping, Var}; + +#[odra::module(events = [Approval, Transfer], errors = Error)] +pub struct Erc20 { + decimals: Var, + symbol: Var, + name: Var, + total_supply: Var, + balances: Mapping, + allowances: Mapping<(Address, Address), U256> +} + +#[odra::module] +impl Erc20 { + pub fn init( + &mut self, + symbol: String, + name: String, + decimals: u8, + initial_supply: Option + ) { + let caller = self.env().caller(); + self.symbol.set(symbol); + self.name.set(name); + self.decimals.set(decimals); + + if let Some(initial_supply) = initial_supply { + self.total_supply.set(initial_supply); + self.balances.set(&caller, initial_supply); + + if !initial_supply.is_zero() { + self.env().emit_event(Transfer { + from: None, + to: Some(caller), + amount: initial_supply + }); + } + } + } + + pub fn transfer(&mut self, recipient: &Address, amount: &U256) { + let caller = self.env().caller(); + self.raw_transfer(&caller, recipient, amount); + } + + pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + let spender = self.env().caller(); + + self.spend_allowance(owner, &spender, amount); + self.raw_transfer(owner, recipient, amount); + } + + pub fn approve(&mut self, spender: &Address, amount: &U256) { + let owner = self.env().caller(); + + self.allowances.set(&(owner, *spender), *amount); + self.env().emit_event(Approval { + owner, + spender: *spender, + value: *amount + }); + } + + pub fn name(&self) -> String { + self.name.get_or_revert_with(Error::NameNotSet) + } + + // Other getter functions... + + pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 { + self.allowances.get_or_default(&(*owner, *spender)) + } + + pub fn mint(&mut self, address: &Address, amount: &U256) { + self.total_supply.add(*amount); + self.balances.add(address, *amount); + + self.env().emit_event(Transfer { + from: None, + to: Some(*address), + amount: *amount + }); + } + + pub fn burn(&mut self, address: &Address, amount: &U256) { + if self.balance_of(address) < *amount { + self.env().revert(Error::InsufficientBalance); + } + self.total_supply.subtract(*amount); + self.balances.subtract(address, *amount); + + self.env().emit_event(Transfer { + from: Some(*address), + to: None, + amount: *amount + }); + } +} + +impl Erc20 { + fn raw_transfer(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + if *amount > self.balances.get_or_default(owner) { + self.env().revert(Error::InsufficientBalance) + } + + self.balances.subtract(owner, *amount); + self.balances.add(recipient, *amount); + + self.env().emit_event(Transfer { + from: Some(*owner), + to: Some(*recipient), + amount: *amount + }); + } + + fn spend_allowance(&mut self, owner: &Address, spender: &Address, amount: &U256) { + let allowance = self.allowances.get_or_default(&(*owner, *spender)); + if allowance < *amount { + self.env().revert(Error::InsufficientAllowance) + } + self.allowances.subtract(&(*owner, *spender), *amount); + + self.env().emit_event(Approval { + owner: *owner, + spender: *spender, + value: allowance - *amount + }); + } +} + +pub mod events { + use odra::prelude::*; + use odra::{casper_types::U256, Address}; + + #[odra::event] + pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 + } + + #[odra::event] + pub struct Approval { + pub owner: Address, + pub spender: Address, + pub value: U256 + } +} + +pub mod errors { + #[odra::odra_error] + pub enum Error { + InsufficientBalance = 30_000, + InsufficientAllowance = 30_001, + NameNotSet = 30_002, + SymbolNotSet = 30_003, + DecimalsNotSet = 30_004 + } +} + +#[cfg(test)] +mod tests { + // nothing changed in the tests +} +``` + + + +```rust title="src/erc20.rs" +use crate::erc20::errors::Error::*; +use crate::erc20::events::*; +use odra::prelude::*; +use odra::{casper_types::U256, Address, Mapping, Var}; + +#[odra::module(events = [Approval, Transfer])] +pub struct Erc20 { + decimals: Var, + symbol: Var, + name: Var, + total_supply: Var, + balances: Mapping, + allowances: Mapping<(Address, Address), U256> +} + +#[odra::module] +impl Erc20 { + pub fn init( + &mut self, + symbol: String, + name: String, + decimals: u8, + initial_supply: Option + ) { + let caller = self.env().caller(); + self.symbol.set(symbol); + self.name.set(name); + self.decimals.set(decimals); + + if let Some(initial_supply) = initial_supply { + self.total_supply.set(initial_supply); + self.balances.set(&caller, initial_supply); + + if !initial_supply.is_zero() { + self.env().emit_event(Transfer { + from: None, + to: Some(caller), + amount: initial_supply + }); + } + } + } + + pub fn transfer(&mut self, recipient: &Address, amount: &U256) { + let caller = self.env().caller(); + self.raw_transfer(&caller, recipient, amount); + } + + pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + let spender = self.env().caller(); + + self.spend_allowance(owner, &spender, amount); + self.raw_transfer(owner, recipient, amount); + } + + pub fn approve(&mut self, spender: &Address, amount: &U256) { + let owner = self.env().caller(); + + self.allowances.set(&(owner, *spender), *amount); + self.env().emit_event(Approval { + owner, + spender: *spender, + value: *amount + }); + } + + pub fn name(&self) -> String { + self.name.get_or_revert_with(NameNotSet) + } + + // Other getter functions... + + pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 { + self.allowances.get_or_default(&(*owner, *spender)) + } + + pub fn mint(&mut self, address: &Address, amount: &U256) { + self.total_supply.add(*amount); + self.balances.add(address, *amount); + + self.env().emit_event(Transfer { + from: None, + to: Some(*address), + amount: *amount + }); + } + + pub fn burn(&mut self, address: &Address, amount: &U256) { + if self.balance_of(address) < *amount { + self.env().revert(InsufficientBalance); + } + self.total_supply.subtract(*amount); + self.balances.subtract(address, *amount); + + self.env().emit_event(Transfer { + from: Some(*address), + to: None, + amount: *amount + }); + } +} + +impl Erc20 { + fn raw_transfer(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + if *amount > self.balances.get_or_default(owner) { + self.env().revert(InsufficientBalance) + } + + self.balances.subtract(owner, *amount); + self.balances.add(recipient, *amount); + + self.env().emit_event(Transfer { + from: Some(*owner), + to: Some(*recipient), + amount: *amount + }); + } + + fn spend_allowance(&mut self, owner: &Address, spender: &Address, amount: &U256) { + let allowance = self.allowances.get_or_default(&(*owner, *spender)); + if allowance < *amount { + self.env().revert(InsufficientAllowance) + } + self.allowances.subtract(&(*owner, *spender), *amount); + + self.env().emit_event(Approval { + owner: *owner, + spender: *spender, + value: allowance - *amount + }); + } +} + +pub mod events { + use odra::prelude::*; + use odra::{casper_types::U256, Address, Event}; + + #[derive(Event, Eq, PartialEq, Debug)] + pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 + } + + #[derive(Event, Eq, PartialEq, Debug)] + pub struct Approval { + pub owner: Address, + pub spender: Address, + pub value: U256 + } +} + +pub mod errors { + use odra::OdraError; + + #[derive(OdraError)] + pub enum Error { + InsufficientBalance = 30_000, + InsufficientAllowance = 30_001, + NameNotSet = 30_002, + SymbolNotSet = 30_003, + DecimalsNotSet = 30_004 + } +} + +#[cfg(test)] +mod tests { + // nothing changed in the tests +} +``` + + + + +## 4. **Troubleshooting** + +If you encounter any further issues after completing the migration steps, please don't hesitate to reach out to us on [Discord] or explore the other sections this documentation. You can also refer to the [technical documentation] for more detailed information. Additionally, our [examples] repository offers a wide range of examples to assist you in understanding the new features and APIs. Be sure to carefully review any compilation errors and warnings, as they may provide valuable insights into the necessary adjustments. + + +## 5. **References** + - [Changelog] + - [Odra Documentation] + - [Docs.rs] + - [Examples] + +[Changelog]: https://github.com/odradev/odra/blob/release/0.9.0/CHANGELOG.md +[templates]: https://github.com/odradev/odra/blob/release/0.9.0/templates +[`HostEnv`]: https://docs.rs/odra/0.9.0/odra/host/struct.HostEnv.html +[`OdraResult`]: https://docs.rs/odra/0.9.0/odra/type.OdraResult.html +[Discord]: https://discord.com/invite/Mm5ABc9P9k +[Odra Documentation]: https://docs.odra.dev +[technical documentation]: https://docs.rs/odra/0.9.0/odra/index.html +[Docs.rs]: https://docs.rs/odra/0.9.0/odra/index.html +[examples]: https:://github.com/odradev/odra/tree/release/0.9.0/examples +[Examples]: https:://github.com/odradev/odra/tree/release/0.9.0/examples +[Casper Contract Schema]: https://github.com/odradev/casper-contract-schema +[previous guide]: ./to-0.8.0 diff --git a/docusaurus/versioned_docs/version-1.2.0/odra-sol.png b/docusaurus/versioned_docs/version-1.2.0/odra-sol.png new file mode 100644 index 000000000..caf92bf53 Binary files /dev/null and b/docusaurus/versioned_docs/version-1.2.0/odra-sol.png differ diff --git a/docusaurus/versioned_docs/version-1.2.0/transactions.png b/docusaurus/versioned_docs/version-1.2.0/transactions.png new file mode 100644 index 000000000..2b3bbd757 Binary files /dev/null and b/docusaurus/versioned_docs/version-1.2.0/transactions.png differ diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/_category_.json b/docusaurus/versioned_docs/version-1.2.0/tutorials/_category_.json new file mode 100644 index 000000000..a941cd1a0 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/_category_.json @@ -0,0 +1,8 @@ +{ + "label": "Tutorials", + "position": 6, + "link": { + "type": "generated-index", + "description": "The theory is good, but the practice is even better. Let's go through a few examples summing up all the Odra concepts." + } +} diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/access-control.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/access-control.md new file mode 100644 index 000000000..e03c56f4e --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/access-control.md @@ -0,0 +1,188 @@ +--- +sidebar_position: 4 +--- + +# Access Control + +In a previous tutorial, we introduced the [`Ownable`](./ownable.md) module, which serves the purpose of securing access to specific contract features. While it establishes a fundamental security layer, there are numerous scenarios where this level of security is insufficient, + +In this article we design and implement a more fine-grained access control layer. + +## Code + +Before we start writing code, we list the functionalities of our access control layer. + +1. A `Role` type is used across the module. +2. A `Role` can be assigned to many `Address`es. +3. Each `Role` may have a corresponding admin role. +4. Only an admin can grant/revoke a `Role`. +5. A `Role` can be renounced. +6. A `Role` cannot be renounced on someone's behalf. +7. Each action triggers an event. +8. Unauthorized access stops contract execution. + +### Project Structure + +```plaintext +access-control +β”œβ”€β”€ src +β”‚ β”œβ”€β”€ access +β”‚ β”‚ β”œβ”€β”€ access_control.rs +β”‚ β”‚ β”œβ”€β”€ events.rs +β”‚ β”‚ └── errors.rs +β”‚ └── lib.rs +|── build.rs +|── Cargo.toml +└── Odra.toml +``` + +### Events and Errors + +There are three actions that can be performed concerning a `Role`: granting, revoking, and altering the admin role. Let us establish standard Odra events for each of these actions. + +```rust title=events.rs showLineNumbers +use odra::prelude::*; +use odra::Address; +use super::access_control::Role; + +#[odra::event] +pub struct RoleGranted { + pub role: Role, + pub address: Address, + pub sender: Address +} + +#[odra::event] +pub struct RoleRevoked { + pub role: Role, + pub address: Address, + pub sender: Address +} + +#[odra::event] +pub struct RoleAdminChanged { + pub role: Role, + pub previous_admin_role: Role, + pub new_admin_role: Role +} +``` +* **L5-L17** - to describe the grant or revoke actions, our events specify the `Role`, and `Address`es indicating who receives or loses access and who provides or withdraws it. +* **L19-L24** - the event describing the admin role change, requires the subject `Role`, the previous and the current admin `Role`. + +```rust title=errors.rs +#[odra::odra_error] +pub enum Error { + MissingRole = 20_000, + RoleRenounceForAnotherAddress = 20_001, +} +``` + +Errors definition is straightforward - there are only two invalid states: +1. An action is triggered by an unauthorized actor. +2. The caller is attempting to resign the Role on someone's behalf. + +### Module + +Now, we are stepping into the most interesting part: the module definition and implementation. + +```rust title=access_control.rs showLineNumbers +use super::events::*; +use super::errors::Error; +use odra::prelude::*; +use odra::{Address, Mapping}; + +pub type Role = [u8; 32]; + +pub const DEFAULT_ADMIN_ROLE: Role = [0u8; 32]; + +#[odra::module(events = [RoleAdminChanged, RoleGranted, RoleRevoked])] +pub struct AccessControl { + roles: Mapping<(Role, Address), bool>, + role_admin: Mapping +} + +#[odra::module] +impl AccessControl { + pub fn has_role(&self, role: &Role, address: &Address) -> bool { + self.roles.get_or_default(&(*role, *address)) + } + + pub fn get_role_admin(&self, role: &Role) -> Role { + let admin_role = self.role_admin.get(role); + if let Some(admin) = admin_role { + admin + } else { + DEFAULT_ADMIN_ROLE + } + } + + pub fn grant_role(&mut self, role: &Role, address: &Address) { + self.check_role(&self.get_role_admin(role), &self.env().caller()); + self.unchecked_grant_role(role, address); + } + + pub fn revoke_role(&mut self, role: &Role, address: &Address) { + self.check_role(&self.get_role_admin(role), &self.env().caller()); + self.unchecked_revoke_role(role, address); + } + + pub fn renounce_role(&mut self, role: &Role, address: &Address) { + if address != &self.env().caller() { + self.env().revert(Error::RoleRenounceForAnotherAddress); + } + self.unchecked_revoke_role(role, address); + } +} + +impl AccessControl { + pub fn check_role(&self, role: &Role, address: &Address) { + if !self.has_role(role, address) { + self.env().revert(Error::MissingRole); + } + } + + pub fn set_admin_role(&mut self, role: &Role, admin_role: &Role) { + let previous_admin_role = self.get_role_admin(role); + self.role_admin.set(role, *admin_role); + self.env().emit_event(RoleAdminChanged { + role: *role, + previous_admin_role, + new_admin_role: *admin_role + }); + } + + pub fn unchecked_grant_role(&mut self, role: &Role, address: &Address) { + if !self.has_role(role, address) { + self.roles.set(&(*role, *address), true); + self.env().emit_event(RoleGranted { + role: *role, + address: *address, + sender: self.env().caller() + }); + } + } + + pub fn unchecked_revoke_role(&mut self, role: &Role, address: &Address) { + if self.has_role(role, address) { + self.roles.set(&(*role, *address), false); + self.env().emit_event(RoleRevoked { + role: *role, + address: *address, + sender: self.env().caller() + }); + } + } +} +``` +* **L6** - Firstly, we need the `Role` type. It is simply an alias for a 32-byte array. +* **L8** - The default role is an array filled with zeros. +* **L10-L13** - The storage consists of two mappings: +1. `roles` - a nested mapping that stores information about whether a certain Role is granted to a given `Address`. +2. `role_admin` - each `Role` can have a single admin `Role`. +* **L18-L20** - This is a simple check to determine if a `Role` has been granted to a given `Address`. It is an exposed entry point and an important building block widely used throughout the entire module. +* **L49** - This is a non-exported block containing helper functions. +* **L50-L54** - The `check_role()` function serves as a guard function. Before a `Role` is granted or revoked, we must ensure that the caller is allowed to do so. For this purpose, the function reads the roles mapping. If the role has not been granted to the address, the contract reverts with `Error::MissingRole`. +* **L56-L64** - The `set_admin_role()` function simply updates the role_admin mapping and emits the `RoleAdminChanged` event. +* **L66-L86** - The `unchecked_grant_role()` and `unchecked_revoke_role()` functions are mirror functions that update the roles mapping and post `RoleGranted` or `RoleRevoked` events. If the role is already granted, `unchecked_grant_role()` has no effect (the opposite check is made in the case of revoking a role). +* **L22-L29** - The `get_role_admin()` entry point reads the role_admin. If there is no admin role for a given role, it returns the default role. +* **L31-L46** - This is a combination of `check_role()` and `unchecked_*_role()`. Entry points fail on unauthorized access. diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/build-deploy-read.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/build-deploy-read.md new file mode 100644 index 000000000..2a050a49b --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/build-deploy-read.md @@ -0,0 +1,509 @@ +--- +sidebar_position: 7 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Build, Deploy and Read the State of a Contract + +In this guide, we will show the full path from creating a contract, deploying it and reading the state. + +We will use a contract with a complex storage layout and show how to deploy it and then read the state of the contract in Rust and TypeScript. + +Before you start, make sure you completed the following steps: +- Read the [Getting Started] guide +- Get familiar with [NCTL tutorial] +- Install [NCTL docker] image +- Install [casper-client] + +### Contract + +Let's write a contract with complex storage layout. + +The contract stores a plain numeric value, a custom nested type and a submodule with another submodule with stores a `Mapping`. + +We will expose two methods: +1. The constructor `init` which sets the metadata and the version of the contract. +2. The method `set_data` which sets the value of the numeric field and the values of the mapping. + +```rust title=custom_item.rs showLineNumbers +use odra::{casper_types::U256, prelude::*, Mapping, SubModule, Var}; + +// A custom type with a vector of another custom type +#[odra::odra_type] +pub struct Metadata { + name: String, + description: String, + prices: Vec, +} + +#[odra::odra_type] +pub struct Price { + value: U256, +} + +// The main contract with a version, metadata and a submodule +#[odra::module] +pub struct CustomItem { + version: Var, + meta: Var, + data: SubModule +} + +#[odra::module] +impl CustomItem { + pub fn init(&mut self, name: String, description: String, price_1: U256, price_2: U256) { + let meta = Metadata { + name, + description, + prices: vec![ + Price { value: price_1 }, + Price { value: price_2 } + ] + }; + self.meta.set(meta); + self.version.set(self.version.get_or_default() + 1); + } + + pub fn set_data(&mut self, value: u32, name: String, name2: String) { + self.data.value.set(value); + self.data.inner.named_values.set(&name, 10); + self.data.inner.named_values.set(&name2, 20); + } +} + +// A submodule with a numeric value and another submodule +#[odra::module] +struct Data { + value: Var, + inner: SubModule, +} + +// A submodule with a mapping +#[odra::module] +struct InnerData { + named_values: Mapping, +} + +``` + +### Deploying the contract + + +First, we need to setup the chain. We will use the NCTL docker image to run a local network. + +``` +docker run --rm -it --name mynctl -d -p 11101:11101 -p 14101:14101 -p 18101:18101 makesoftware/casper-nctl +``` + +Next, we need to compile the contract to a Wasm file. + +```sh +cargo odra build -c custom_item +``` + +Then, we can deploy the contract using the `casper-client` tool. + +```sh +casper-client put-deploy \ + --node-address http://localhost:11101 \ + --chain-name casper-net-1 \ + --secret-key path/to/your/secret_key.pem \ + --session-path [PATH_TO_WASM] \ + --payment-amount 100000000000 \ + --session-arg "odra_cfg_package_hash_key_name:string:'test_contract_package_hash'" \ + --session-arg "odra_cfg_allow_key_override:bool:'true'" \ + --session-arg "odra_cfg_is_upgradable:bool:'true'" \ + --session-arg "name:string='My Name'" \ + --session-arg "description:string='My Description'" \ + --session-arg "price_1:u256='101'" \ + --session-arg "price_2:u256='202'" +``` + +Finally, we can call the `set_data` method to set the values of the contract. + +```sh +casper-client put-deploy \ + --node-address http://localhost:11101 \ + --chain-name casper-net-1 \ + --secret-key ./keys/secret_key.pem \ + --payment-amount 2000000000 \ + --session-hash [DEPLOYED_CONTRACT_HASH] \ + --session-entry-point "set_data" \ + --session-arg "value:u32:'666'" \ + --session-arg "name:string='alice'" \ + --session-arg "name2:string='bob'" +``` + +### Storage Layout + +To read the state of the contract, we need to understand the storage layout. + +The first step is to calculate the index of the keys. + +``` +Storage Layout + +CustomItem: prefix: 0x0..._0000_0000_0000 0 + version: u32, 0x0..._0000_0000_0001 1 + meta: Metadata, 0x0..._0000_0000_0010 2 + data: Data: prefix: 0x0..._0000_0000_0011 3 + value: u32, 0x0..._0000_0011_0001 (3 << 4) + 1 + inner: InnerData: prefix: 0x0..._0000_0011_0010 (3 << 4) + 2 + named_values: Mapping 0x0..._0011_0010_0001 ((3 << 4) + 2) << 4 + 1 +``` + +The actual key is obtained as follows: +1. Convert the index to a big-endian byte array. +2. Concatenate the index with the mapping data. +3. Hash the concatenated bytes using blake2b. +4. Return the hex representation of the hash (the stored key must be utf-8 encoded). + +In more detail, the storage layout is described in the [Storage Layout article](../advanced/04-storage-layout.md). + +### Reading the state + + + + +```rust title=main.rs showLineNumbers +use casper_client::{rpcs::DictionaryItemIdentifier, types::StoredValue, Verbosity}; +use casper_types::{ + bytesrepr::{FromBytes, ToBytes}, + U256, +}; + +// replace with your contract hash +const CONTRACT_HASH: &str = "hash-..."; +const NODE_ADDRESS: &str = "http://localhost:11101/rpc"; +const RPC_ID: &str = "casper-net-1"; +const DICTIONARY_NAME: &str = "state"; + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Metadata { + name: String, + description: String, + prices: Vec, +} + +#[derive(Debug, PartialEq, Eq, Hash)] +pub struct Price { + value: U256, +} + +async fn read_state_key(key: String) -> Vec { + let state_root_hash = casper_client::get_state_root_hash( + RPC_ID.to_string().into(), + NODE_ADDRESS, + Verbosity::Low, + None, + ) + .await + .unwrap() + .result + .state_root_hash + .unwrap(); + + // Read the value from the `state` dictionary. + let result = casper_client::get_dictionary_item( + RPC_ID.to_string().into(), + NODE_ADDRESS, + Verbosity::Low, + state_root_hash, + DictionaryItemIdentifier::ContractNamedKey { + key: CONTRACT_HASH.to_string(), + dictionary_name: DICTIONARY_NAME.to_string(), + dictionary_item_key: key, + }, + ) + .await + .unwrap() + .result + .stored_value; + + // We expect the value to be a CLValue + if let StoredValue::CLValue(cl_value) = result { + // Ignore the first 4 bytes, which are the length of the CLType. + cl_value.inner_bytes()[4..].to_vec() + } else { + vec![] + } +} + +async fn metadata() -> Metadata { + // The key for the metadata is 2, and it has no mapping data + let key = key(2, &[]); + let bytes = read_state_key(key).await; + + // Read the name and store the remaining bytes + let (name, bytes) = String::from_bytes(&bytes).unwrap(); + // Read the description and store the remaining bytes + let (description, bytes) = String::from_bytes(&bytes).unwrap(); + // A vector is stored as a u32 size followed by the elements + // Read the size of the vector and store the remaining bytes + let (size, mut bytes) = u32::from_bytes(&bytes).unwrap(); + + let mut prices = vec![]; + // As we know the size of the vector, we can loop over it + for _ in 0..size { + // Read the value and store the remaining bytes + let (value, rem) = U256::from_bytes(&bytes).unwrap(); + bytes = rem; + prices.push(Price { value }); + } + // Anytime you finish parsing a value, you should check if there are any remaining bytes + // if there are, it means you have a bug in your parsing logic. + // For simplicity, we will ignore the remaining bytes here. + Metadata { + name, + description, + prices + } +} + +async fn value() -> u32 { + // The key for the value is (3 << 4) + 1, and it has no mapping data + let key = key((3 << 4) + 1, &[]); + let bytes = read_state_key(key).await; + + // Read the value and ignore the remaining bytes for simplicity + u32::from_bytes(&bytes).unwrap().0 +} + +async fn named_value(name: &str) -> u32 { + // The key for the named value is (((3 << 4) + 2) << 4) + 1, and the mapping data is the name as bytes + let mapping_data = name.to_bytes().unwrap(); + let key = key((((3 << 4) + 2) << 4) + 1, &mapping_data); + let bytes = read_state_key(key).await; + + // Read the value and ignore the remaining bytes for simplicity + u32::from_bytes(&bytes).unwrap().0 +} + +fn main() { + let runtime = tokio::runtime::Runtime::new().unwrap(); + dbg!(runtime.block_on(metadata())); + dbg!(runtime.block_on(value())); + dbg!(runtime.block_on(named_value("alice"))); + dbg!(runtime.block_on(named_value("bob"))); +} + +// The key is a combination of the index and the mapping data +// The algorithm is as follows: +// 1. Convert the index to a big-endian byte array +// 2. Concatenate the index with the mapping data +// 3. Hash the concatenated bytes using blake2b +// 4. Return the hex representation of the hash (the stored key must be utf-8 encoded) +fn key(idx: u32, mapping_data: &[u8]) -> String { + let mut key = Vec::new(); + key.extend_from_slice(idx.to_be_bytes().as_ref()); + key.extend_from_slice(mapping_data); + let hashed_key = blake2b(&key); + + hex::encode(&hashed_key) +} + +fn blake2b(bytes: &[u8]) -> [u8; 32] { + let mut result = [0u8; 32]; + let mut hasher = ::new(32) + .expect("should create hasher"); + let _ = std::io::Write::write(&mut hasher, bytes); + blake2::digest::VariableOutput::finalize_variable(hasher, &mut result) + .expect("should copy hash to the result array"); + result +} + +``` + +```sh +cargo run +[src/main.rs:116:5] runtime.block_on(metadata()) = Metadata { + name: "My Contract", + description: "My Description", + prices: [ + Price { + value: 123, + }, + Price { + value: 321, + }, + ], +} +[src/main.rs:117:5] runtime.block_on(value()) = 666 +[src/main.rs:118:5] runtime.block_on(named_value("alice")) = 20 +[src/main.rs:119:5] runtime.block_on(named_value("bob")) = 10 +``` + + + + + +```typescript title=index.ts showLineNumbers + +import { blake2bHex } from "blakejs"; +import { + CLList, + CLListBytesParser, + CLStringBytesParser, + CLU256BytesParser, + CLU32BytesParser, + CLU8, + CLValueBuilder, + CasperClient, + CasperServiceByJsonRPC, + Contracts, + ToBytes, +} from "casper-js-sdk"; + +const LOCAL_NODE_URL = "http://127.0.0.1:11101/rpc"; +// replace with your contract hash +const CONTRACT_HASH = "hash-..."; +const STATE_DICTIONARY_NAME = "state"; +const U32_SIZE = 4; + +class Price { + value: bigint; + + constructor(value: bigint) { + this.value = value; + } +} + +class Metadata { + name: string; + description: string; + prices: Price[]; + + constructor(name: string, description: string, prices: Price[]) { + this.name = name; + this.description = description; + this.prices = prices; + } +} + +export class Contract { + client: CasperClient; + service: CasperServiceByJsonRPC; + contract: Contracts.Contract; + + private constructor() { + this.client = new CasperClient(LOCAL_NODE_URL); + this.service = new CasperServiceByJsonRPC(LOCAL_NODE_URL); + this.contract = new Contracts.Contract(this.client); + this.contract.setContractHash(CONTRACT_HASH); + } + + static async load() { + return new Contract(); + } + + async read_state(key: string) { + const response = await this.contract.queryContractDictionary(STATE_DICTIONARY_NAME, key); + let data: CLList = CLValueBuilder.list(response.value()); + let bytes = new CLListBytesParser().toBytes(data).unwrap(); + // Ignore the first 4 bytes, which are the length of the CLType + return bytes.slice(4); + } + + async metadata() { + // The key for the metadata is 2, and it has no mapping data + let bytes: Uint8Array = await this.read_state(key(2)); + + // Read the name and store the remaining bytes + let name = new CLStringBytesParser().fromBytesWithRemainder(bytes); + bytes = name.remainder as Uint8Array; + + // Read the description and store the remaining bytes + let description = new CLStringBytesParser().fromBytesWithRemainder(bytes); + bytes = description.remainder as Uint8Array; + + let prices: Price[] = []; + // A vector is stored as a u32 size followed by the elements + // Read the size of the vector and store the remaining bytes + let size = new CLU32BytesParser().fromBytesWithRemainder(bytes); + bytes = size.remainder as Uint8Array; + + // As we know the size of the vector, we can loop over it + for (let i = 0; i < size.result.unwrap().data.toNumber(); i++) { + let price = new CLU256BytesParser().fromBytesWithRemainder(bytes); + bytes = price.remainder as Uint8Array; + prices.push(new Price(price.result.unwrap().data.toBigInt())); + } + + // Anytime you finish parsing a value, you should check if there are any remaining bytes + // if there are, it means you have a bug in your parsing logic. + // For simplicity, we will ignore the remaining bytes here. + return new Metadata( + name.result.unwrap().data, + description.result.unwrap().data, + prices + ); + } + + async value() { + // The key for the value is (3 << 4) + 1, and it has no mapping data + const bytes = await this.read_state(key((3 << 4) + 1)); + + // Read the value and ignore the remaining bytes for simplicity + let value = new CLU32BytesParser().fromBytesWithRemainder(bytes); + return value.result.unwrap().data.toBigInt(); + } + + async named_value(name: string) { + // The key for the named value is (((3 << 4) + 2) << 4) + 1, and the mapping data is the name as bytes + let mapping_data = new CLStringBytesParser() + .toBytes(CLValueBuilder.string(name)) + .unwrap(); + let bytes: Uint8Array = await this.read_state( + key((((3 << 4) + 2) << 4) + 1, mapping_data) + ); + + // Read the value and ignore the remaining bytes for simplicity + let value = new CLU32BytesParser().fromBytesWithRemainder(bytes); + return value.result.unwrap().data.toBigInt(); + } +} + +// The key is a combination of the index and the mapping data +// The algorithm is as follows: +// 1. Convert the index to a big-endian byte array +// 2. Concatenate the index with the mapping data +// 3. Hash the concatenated bytes using blake2b +// 4. Return the hex representation of the hash (the stored key must be utf-8 encoded) +function key(idx: number, mapping_data: Uint8Array = new Uint8Array([])) { + let key = new Uint8Array(U32_SIZE + mapping_data.length); + new DataView(key.buffer).setUint32(0, idx, false); // false for big-endian + key.set(mapping_data, U32_SIZE); + + return blake2bHex(key, undefined, 32); +} + +const contract = Contract.load(); +contract.then(async (c) => { + console.log(await c.value()); + console.log(await c.metadata()); + console.log(await c.named_value("alice")); + console.log(await c.named_value("bob")); +}); +``` + +```sh +tsc && node target/index.js +Metadata { + name: 'My Contract', + description: 'My Description', + prices: [ Price { value: 123n }, Price { value: 321n } ] +} +666n +20n +10n +``` + + + + +[Getting Started]: ../category/getting-started +[NCTL tutorial]: https://docs.casper.network/developers/dapps/setup-nctl/ +[NCTL docker]: https://github.com/make-software/casper-nctl-docker +[casper-client]: https://github.com/casper-ecosystem/casper-client-rs diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/cep18.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/cep18.md new file mode 100644 index 000000000..a46c1e39a --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/cep18.md @@ -0,0 +1,592 @@ +--- +sidebar_position: 9 +--- + +# CEP-18 +Not so different from ERC-20, the CEP-18 standard describes a fungible +token interface, but for the Casper network. +There are some differences, which will be shown in this tutorial. +The most visible one however, is the compatibility with the Casper Ecosystem. + +In our example, we will implement a CEP-18 token with a simple self-governance mechanism. +We will also deploy our token on the Casper network, and interact with it. + +:::warning +This implementation of the governance in this tutorial is by no means +a complete one, and should not be used in production. +::: + +## Self-governing token +There are many ways to implement a governance mechanism for a token, +each more complex than the other. In our example, we will use a simple +one, where the community of token holders can vote to mint new tokens. + +## Token implementation + +Let's start by creating a new project, choosing a clever name and using +cep18 as our starting template: + +```bash +cargo odra new --name ourcoin --template cep18 +``` + +Let's glance at our token code: + +```rust showLineNumbers title="src/token.rs" +#[odra::module] +pub struct MyToken { + token: SubModule, +} + +impl MyToken { + // Delegate all Cep18 functions to the token sub-module. + delegate! { + to self.token { + ... + fn name(&self) -> String; + fn symbol(&self) -> String; + ... +``` + +As we can see, it indeed uses the `Cep18` module and delegates +all the methods to it. + +The only thing to do is to change the name of the struct to more +appropriate `OurToken`, run the provided tests using `cargo odra test`, +and continue with the implementation of the governance. + +:::note +Remember to change the name of the struct and its usages as well as +the struct name in the `Odra.toml` file! +::: + +## Governance implementation + +Let's go through the process of implementing the governance mechanism. +If we don't want to, we don't have to hide entrypoints from the public responsible +for minting new tokens. By default, minting [Modality](https://github.com/casper-ecosystem/cep18/tree/dev/cep18#modalities) +is turned off, so any attempt of direct minting will result in an error. + +We will however implement a voting mechanism, where the token holders can vote +to mint new tokens. + +### Voting mechanism +Our voting system will be straightforward: + +1. Anyone with the tokens can propose a new mint. +2. Anyone with the tokens can vote for the new mint by staking their tokens. +3. If the majority of the token holders vote for the mint, it is executed. + +#### Storage + +We will need to store some additional information about the votes, so let's +add some fields to our token struct: + +```rust showLineNumbers title="src/token.rs" +#[odra::module] +pub struct OurToken { + /// A sub-module that implements the CEP-18 token standard. + token: SubModule, + /// The proposed mint. + proposed_mint: Var<(Address, U256)>, + /// The list of votes cast in the current vote. + votes: List, + /// Whether a vote is open. + is_vote_open: Var, + /// The time when the vote ends. + vote_end_time: Var, +} + +/// A ballot cast by a voter. +#[odra::odra_type] +struct Ballot { + voter: Address, + choice: bool, + amount: U256, +} +``` +Notice that `proposed_mint` contains a tuple containing the address of +the proposer and the amount of tokens to mint. Moreover, we need to keep track if +the vote time has ended, but also if it was already tallied, that's why +we need both `is_vote_open` and `vote_end_time`. + +We will also use the power of the [List](../basics/storage-interaction#list) +type to store the `Ballots`. + +#### Proposing a new mint +To implement the endpoint that allows token holders to propose a new mint, +we need to add a new function to our token module: + +```rust showLineNumbers title="src/token.rs" +/// Proposes a new mint for the contract. +pub fn propose_new_mint(&mut self, account: Address, amount: U256) { + // Only allow proposing a new mint if there is no vote in progress. + if self.is_vote_open().get_or_default() { + self.env().revert(GovernanceError::VoteAlreadyOpen); + } + + // Only the token holders can propose a new mint. + if self.balance_of(&self.env().caller()) == U256::zero() { + self.env().revert(GovernanceError::OnlyTokenHoldersCanPropose); + } + + // Set the proposed mint. + self.proposed_mint.set((account, amount)); + // Open a vote. + self.is_vote_open.set(true); + // Set the vote end time to 10 minutes from now. + self.vote_end_time + .set(self.env().get_block_time() + 60 * 10 * 1000); +} +``` + +As a parameters to the function, we pass the address of the account that should be the receiver of +the minted tokens, and the amount. + +After some validation, we open the vote by setting the `is_vote_open` to `true`, +and setting the `vote_end_time` to 10 minutes. In real-world scenarios, +the time could be configurable, but for the sake of simplicity, we hardcoded it. +Also, it should be quite longer than 10 minutes, but it will come in handy +when we test it on Livenet. + +#### Voting for the mint + +Next, we need an endpoint that will allow us to cast a ballot: + +```rust showLineNumbers title="src/token.rs" +/// Votes on the proposed mint. +pub fn vote(&mut self, choice: bool, amount: U256) { + // Only allow voting if there is a vote in progress. + self.assert_vote_in_progress(); + + let voter = self.env().caller(); + let contract = self.env().self_address(); + + // Transfer the voting tokens from the voter to the contract. + self.token + .transfer(&contract, &amount); + + // Add the vote to the list. + self.votes.push(Ballot { + voter, + choice, + amount, + }); +} +``` + +The most interesting thing here is that we are using a mechanism of staking, +where we transfer our tokens to the contract, to show that we really mean it. + +The tokens will be locked until the vote is over, and tallied. + +Speaking of tallying... + +#### Tallying the votes + +The last step is to tally the votes and mint the tokens if the majority +of voters agreed to do so: + +```rust showLineNumbers title="src/token.rs" +/// Count the votes and perform the action +pub fn tally(&mut self) { + // Only allow tallying the votes once. + if !self.is_vote_open.get_or_default() + { + self.env().revert(GovernanceError::NoVoteInProgress); + } + + // Only allow tallying the votes after the vote has ended. + let finish_time = self + .vote_end_time + .get_or_revert_with(GovernanceError::NoVoteInProgress); + if self.env().get_block_time() < finish_time { + self.env().revert(GovernanceError::VoteNotYetEnded); + } + + // Count the votes + let mut yes_votes = U256::zero(); + let mut no_votes = U256::zero(); + + let contract = self.env().self_address(); + + while let Some(vote) = self.votes.pop() { + if vote.choice { + yes_votes += vote.amount; + } else { + no_votes += vote.amount; + } + + // Transfer back the voting tokens to the voter. + self.token.raw_transfer(&contract, &vote.voter, &vote.amount); + } + + // Perform the action if the vote has passed. + if yes_votes > no_votes { + let (account, amount) = self + .proposed_mint + .get_or_revert_with(GovernanceError::NoVoteInProgress); + self.token.raw_mint(&account, &amount); + } + + // Close the vote. + self.is_vote_open.set(false); +} +``` + +Notice how we used `raw_transfer` from the `Cep18` module. We used it +to set the sender, so the contract's balance will be used, instead of +the caller's. + +Additonally, we used `raw_mint` to mint the tokens, skipping the security +checks. We have no modality for minting, but even if we had, we don't +have anyone with permissions! The Contract needs to mint the tokens itself. + +### Testing + +Now, we will put our implementation to the test. One unit test, that we can +run both on OdraVM and on the CasperVM. + +```rust showLineNumbers title="src/token.rs" +#[test] +fn it_works() { + let env = odra_test::env(); + let init_args = OurTokenInitArgs { + name: "OurToken".to_string(), + symbol: "OT".to_string(), + decimals: 0, + initial_supply: U256::from(1_000u64), + }; + + let mut token = OurTokenHostRef::deploy(&env, init_args); + + // The deployer, as the only token holder, + // starts a new voting to mint 1000 tokens to account 1. + // There is only 1 token holder, so there is one Ballot cast. + token.propose_new_mint(env.get_account(1), U256::from(2000)); + token.vote(true, U256::from(1000)); + + // The tokens should now be staked. + assert_eq!(token.balance_of(&env.get_account(0)), U256::zero()); + + // Wait for the vote to end. + env.advance_block_time(60 * 11 * 1000); + + // Finish the vote. + token.tally(); + + // The tokens should now be minted. + assert_eq!(token.balance_of(&env.get_account(1)), U256::from(2000)); + assert_eq!(token.total_supply(), 3000.into()); + + // The stake should be returned. + assert_eq!(token.balance_of(&env.get_account(0)), U256::from(1000)); + + // Now account 1 can mint new tokens with their voting power... + env.set_caller(env.get_account(1)); + token.propose_new_mint(env.get_account(1), U256::from(2000)); + token.vote(true, U256::from(2000)); + + // ...Even if the deployer votes against it. + env.set_caller(env.get_account(0)); + token.vote(false, U256::from(1000)); + + env.advance_block_time(60 * 11 * 1000); + + token.tally(); + + // The power of community governance! + assert_eq!(token.balance_of(&env.get_account(1)), U256::from(4000)); +} +``` + +We can run the test using both methods: + +```bash +cargo odra test +cargo odra test -b casper +``` + +It is all nice and green, but it would be really nice to see it in action. + +How about deploying it on the Casper network? + + +## What's next +We will se our token in action, by [deploying it on the Casper network](deploying-on-casper), +and using tools from the Casper Ecosystem to interact with it. + +## Complete code + +Here is the complete code of the `OurToken` module: + +```rust showLineNumbers title="src/token.rs" +use odra::{casper_types::U256, prelude::*, Address, List, SubModule, Var}; +use odra_modules::cep18_token::Cep18; + +/// A ballot cast by a voter. +#[odra::odra_type] +struct Ballot { + voter: Address, + choice: bool, + amount: U256, +} + +/// Errors for the governed token. +#[odra::odra_error] +pub enum GovernanceError { + /// The vote is already in progress. + VoteAlreadyOpen = 0, + /// No vote is in progress. + NoVoteInProgress = 1, + /// Cannot tally votes yet. + VoteNotYetEnded = 2, + /// Vote ended + VoteEnded = 3, + /// Only the token holders can propose a new mint. + OnlyTokenHoldersCanPropose = 4, +} + +/// A module definition. Each module struct consists of Vars and Mappings +/// or/and other modules. +#[odra::module] +pub struct OurToken { + /// A submodule that implements the CEP-18 token standard. + token: SubModule, + /// The proposed mint. + proposed_mint: Var<(Address, U256)>, + /// The list of votes cast in the current vote. + votes: List, + /// Whether a vote is open. + is_vote_open: Var, + /// The time when the vote ends. + vote_end_time: Var, +} +/// Module implementation. +/// +/// To generate entrypoints, +/// an implementation block must be marked as #[odra::module]. +#[odra::module] +impl OurToken { + /// Initializes the contract with the given metadata and initial supply. + pub fn init(&mut self, name: String, symbol: String, decimals: u8, initial_supply: U256) { + // We put the token address as an admin, so it can govern itself. Self-governing token! + self.token + .init(symbol, name, decimals, initial_supply, vec![], vec![], None); + } + + // Delegate all Cep18 functions to the token submodule. + delegate! { + to self.token { + /// Admin EntryPoint to manipulate the security access granted to users. + /// One user can only possess one access group badge. + /// Change strength: None > Admin > Minter + /// Change strength meaning by example: If a user is added to both Minter and Admin, they will be an + /// Admin, also if a user is added to Admin and None then they will be removed from having rights. + /// Beware: do not remove the last Admin because that will lock out all admin functionality. + fn change_security( + &mut self, + admin_list: Vec
, + minter_list: Vec
, + none_list: Vec
+ ); + + /// Returns the name of the token. + fn name(&self) -> String; + + /// Returns the symbol of the token. + fn symbol(&self) -> String; + + /// Returns the number of decimals the token uses. + fn decimals(&self) -> u8; + + /// Returns the total supply of the token. + fn total_supply(&self) -> U256; + + /// Returns the balance of the given address. + fn balance_of(&self, address: &Address) -> U256; + + /// Returns the amount of tokens the owner has allowed the spender to spend. + fn allowance(&self, owner: &Address, spender: &Address) -> U256; + + /// Approves the spender to spend the given amount of tokens on behalf of the caller. + fn approve(&mut self, spender: &Address, amount: &U256); + + /// Decreases the allowance of the spender by the given amount. + fn decrease_allowance(&mut self, spender: &Address, decr_by: &U256); + + /// Increases the allowance of the spender by the given amount. + fn increase_allowance(&mut self, spender: &Address, inc_by: &U256); + + /// Transfers tokens from the caller to the recipient. + fn transfer(&mut self, recipient: &Address, amount: &U256); + + /// Transfers tokens from the owner to the recipient using the spender's allowance. + fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256); + + /// Mints new tokens and assigns them to the given address. + fn mint(&mut self, owner: &Address, amount: &U256); + + /// Burns the given amount of tokens from the given address. + fn burn(&mut self, owner: &Address, amount: &U256); + } + } + + /// Proposes a new mint for the contract. + pub fn propose_new_mint(&mut self, account: Address, amount: U256) { + // Only allow proposing a new mint if there is no vote in progress. + if self.is_vote_open.get_or_default() { + self.env().revert(GovernanceError::VoteAlreadyOpen); + } + + // Only the token holders can propose a new mint. + if self.balance_of(&self.env().caller()) == U256::zero() { + self.env() + .revert(GovernanceError::OnlyTokenHoldersCanPropose); + } + + // Set the proposed mint. + self.proposed_mint.set((account, amount)); + // Open a vote. + self.is_vote_open.set(true); + // Set the vote end time to 10 minutes from now. + self.vote_end_time + .set(self.env().get_block_time() + 10 * 60 * 1000); + } + + /// Votes on the proposed mint. + pub fn vote(&mut self, choice: bool, amount: U256) { + // Only allow voting if there is a vote in progress. + self.assert_vote_in_progress(); + + let voter = self.env().caller(); + let contract = self.env().self_address(); + + // Transfer the voting tokens from the voter to the contract. + self.token.transfer(&contract, &amount); + + // Add the vote to the list. + self.votes.push(Ballot { + voter, + choice, + amount, + }); + } + + /// Count the votes and perform the action + pub fn tally(&mut self) { + // Only allow tallying the votes once. + if !self.is_vote_open.get_or_default() { + self.env().revert(GovernanceError::NoVoteInProgress); + } + + // Only allow tallying the votes after the vote has ended. + let finish_time = self + .vote_end_time + .get_or_revert_with(GovernanceError::NoVoteInProgress); + if self.env().get_block_time() < finish_time { + self.env().revert(GovernanceError::VoteNotYetEnded); + } + + // Count the votes + let mut yes_votes = U256::zero(); + let mut no_votes = U256::zero(); + + let contract = self.env().self_address(); + + while let Some(vote) = self.votes.pop() { + if vote.choice { + yes_votes += vote.amount; + } else { + no_votes += vote.amount; + } + + // Transfer back the voting tokens to the voter. + self.token + .raw_transfer(&contract, &vote.voter, &vote.amount); + } + + // Perform the action if the vote has passed. + if yes_votes > no_votes { + let (account, amount) = self + .proposed_mint + .get_or_revert_with(GovernanceError::NoVoteInProgress); + self.token.raw_mint(&account, &amount); + } + + // Close the vote. + self.is_vote_open.set(false); + } + + fn assert_vote_in_progress(&self) { + if !self.is_vote_open.get_or_default() { + self.env().revert(GovernanceError::NoVoteInProgress); + } + + let finish_time = self + .vote_end_time + .get_or_revert_with(GovernanceError::NoVoteInProgress); + + if self.env().get_block_time() > finish_time { + self.env().revert(GovernanceError::VoteEnded); + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use odra::host::Deployer; + + #[test] + fn it_works() { + let env = odra_test::env(); + let init_args = OurTokenInitArgs { + name: "OurToken".to_string(), + symbol: "OT".to_string(), + decimals: 0, + initial_supply: U256::from(1_000u64), + }; + + let mut token = OurTokenHostRef::deploy(&env, init_args); + + // The deployer, as the only token holder, + // starts a new voting to mint 1000 tokens to account 1. + // There is only 1 token holder, so there is one Ballot cast. + token.propose_new_mint(env.get_account(1), U256::from(2000)); + token.vote(true, U256::from(1000)); + + // The tokens should now be staked. + assert_eq!(token.balance_of(&env.get_account(0)), U256::zero()); + + // Wait for the vote to end. + env.advance_block_time(60 * 11 * 1000); + + // Finish the vote. + token.tally(); + + // The tokens should now be minted. + assert_eq!(token.balance_of(&env.get_account(1)), U256::from(2000)); + assert_eq!(token.total_supply(), 3000.into()); + + // The stake should be returned. + assert_eq!(token.balance_of(&env.get_account(0)), U256::from(1000)); + + // Now account 1 can mint new tokens with their voting power... + env.set_caller(env.get_account(1)); + token.propose_new_mint(env.get_account(1), U256::from(2000)); + token.vote(true, U256::from(2000)); + + // ...Even if the deployer votes against it. + env.set_caller(env.get_account(0)); + token.vote(false, U256::from(1000)); + + env.advance_block_time(60 * 11 * 1000); + + token.tally(); + + // The power of community governance! + assert_eq!(token.balance_of(&env.get_account(1)), U256::from(4000)); + } +} +``` diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/deploying-on-casper.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/deploying-on-casper.md new file mode 100644 index 000000000..20faadf05 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/deploying-on-casper.md @@ -0,0 +1,302 @@ +--- +sidebar_position: 10 +--- + +# Deploying a Token on Casper Livenet +In this tutorial, we will take the token we created in +the previous one and deploy it on the Livenet Casper network, +using the Odra Livenet backend. + +We will also take a look at the tools that Casper Ecosystem +provides to interact with our newly deployed token. + +:::info +Most of this tutorial will work with any Casper contract. +::: + +## Casper Wallet + +We will be using Casper Wallet to do some tasks in this tutorial. +To install it, please follow the instructions on the +[official website](https://www.casperwallet.io/). + +After setting up the wallet, extract the private key of the account +you want to use for our testing. +You can do this by clicking on the Menu > Download account keys. + +:::warning +You are solely responsible for the security of your private keys. +We recommend creating a new account for the testing purposes. +::: + +Why do we need the private key? We will use it in Odra to deploy +our contract to the Casper network using Livenet backend. + +## Getting tokens +To deploy the contract on the Livenet, we need to have some CSPR. +The easiest way to get them is to use the faucet, which will send +us 1000 CSPR for free. Unfortunately, only on the Testnet. + +To use the faucet, go to the [Casper Testnet Faucet](https://testnet.cspr.live/tools/faucet). +Log in using your Casper Wallet account and click on the "Request Tokens" button. + +:::note +One account can request tokens only once. If you run out of tokens, you can +either ask someone in the Casper community to send you some, or simply create a new account +in the wallet. +::: + +Now, when we have the tokens, we can deploy the contract. Let's do it using Odra! + +## Odra Livenet +Odra Livenet is described in detail in the +[backends section](../backends/livenet) of this documentation. +We will then briefly describe how to use set it up in this tutorial. + +In your contract code, create a new file in the bin folder: + +```rust title="bin/our_token_livenet.rs" +//! Deploys a new OurToken contract on the Casper livenet and mints some tokens for the tutorial +//! creator. +use std::str::FromStr; + +use odra::casper_types::U256; +use odra::host::{Deployer, HostEnv, HostRef, HostRefLoader}; +use odra::Address; +use ourcoin::token::{OurTokenHostRef, OurTokenInitArgs}; + +fn main() { + // Load the Casper livenet environment. + let env = odra_casper_livenet_env::env(); + + // Caller is the deployer and the owner of the private key. + let owner = env.caller(); + // Just some random address... + let recipient = "hash-48bd92253a1370d1d913c56800296145547a243d13ff4f059ba4b985b1e94c26"; + let recipient = Address::from_str(recipient).unwrap(); + + // Deploy new contract. + let mut token = deploy_our_token(&env); + println!("Token address: {}", token.address().to_string()); + + // Propose minting new tokens. + env.set_gas(1_000_000_000u64); + token.propose_new_mint(recipient, U256::from(1_000)); + + // Vote, we are the only voter. + env.set_gas(1_000_000_000u64); + token.vote(true, U256::from(1_000)); + + // Let's advance the block time by 11 minutes, as + // we set the voting time to 10 minutes. + // OH NO! It is the Livenet, so we need to wait real time... + // Hopefully you are not in a hurry. + env.advance_block_time(11 * 60 * 1000); + + // Tally the votes. + env.set_gas(1_500_000_000u64); + token.tally(); + + // Check the balances. + println!("Owner's balance: {:?}", token.balance_of(&owner)); + println!( + "Tutorial creator's balance: {:?}", + token.balance_of(&recipient) + ); +} + +/// Deploys a contract. +pub fn deploy_our_token(env: &HostEnv) -> OurTokenHostRef { + let name = String::from("OurToken"); + let symbol = String::from("OT"); + let decimals = 0; + let initial_supply = U256::from(1_000); + + let init_args = OurTokenInitArgs { + name, + symbol, + decimals, + initial_supply, + }; + + env.set_gas(300_000_000_000u64); + OurTokenHostRef::deploy(env, init_args) +} + +/// Loads a contract. Just in case you need to load an existing contract later... +fn _load_cep18(env: &HostEnv) -> OurTokenHostRef { + let address = "hash-XXXXX"; + let address = Address::from_str(address).unwrap(); + OurTokenHostRef::load(env, address) +} +``` + +In your `Cargo.toml` file, we need to add a new dependency, a feature and +register the new binary. In the end, it should look like this: + +```toml title="Cargo.toml" +[package] +name = "ourcoin" +version = "0.1.0" +edition = "2021" + +[dependencies] +odra = { version = "1.1.0", features = [], default-features = false } +odra-modules = { version = "1.1.0", features = [], default-features = false } +odra-casper-livenet-env = { version = "1.1.0", optional = true } + +[dev-dependencies] +odra-test = { version = "1.1.0", features = [], default-features = false } + +[build-dependencies] +odra-build = { version = "1.1.0", features = [], default-features = false } + +[features] +default = [] +livenet = ["odra-casper-livenet-env"] + +[[bin]] +name = "ourcoin_build_contract" +path = "bin/build_contract.rs" +test = false + +[[bin]] +name = "ourcoin_build_schema" +path = "bin/build_schema.rs" +test = false + +[[bin]] +name = "our_token_livenet" +path = "bin/our_token_livenet.rs" +required-features = ["livenet"] + +[profile.release] +codegen-units = 1 +lto = true + +[profile.dev.package."*"] +opt-level = 3 +``` + +Finally, add the `.env` file with the following content: + +```env title=".env" +# Path to the secret key of the account that will be used to deploy the contracts. +ODRA_CASPER_LIVENET_SECRET_KEY_PATH=folder_with_your_secret_key/secret_key_file.pem + +# RPC address of the node that will be used to deploy the contracts. +ODRA_CASPER_LIVENET_NODE_ADDRESS=http://138.201.80.141:7777 + +# Chain name of the network. +ODRA_CASPER_LIVENET_CHAIN_NAME=casper-test +``` + +Of course, you need to replace the secret key's path +with the path to the secret key file you downloaded from the Casper Wallet. + +:::note +One of the problems you may encounter is that the node you are using +will be down or will not accept your calls. In this case, you will +have to find and use another node IP address. +::: + +Now, we will run our code: + +```bash +cargo run --bin our_token_livenet --features livenet +``` + +If everything is set up correctly, you should see the output similar to this: + +``` + Running `target/debug/our_token_livenet` +πŸ’ INFO : Deploying "OurToken". +πŸ’ INFO : Found wasm under "wasm/OurToken.wasm". +πŸ™„ WAIT : Waiting 15s for "e6b34772ebc3682702674102db87c633b0544242eafd5944e680371be4ea1227". +πŸ™„ WAIT : Waiting 15s for "e6b34772ebc3682702674102db87c633b0544242eafd5944e680371be4ea1227". +πŸ’ INFO : Deploy "e6b34772ebc3682702674102db87c633b0544242eafd5944e680371be4ea1227" successfully executed. +πŸ’ INFO : Contract "hash-565bd0bde39c8c3dd79e49c037e05eac8add2b2193e86a91a6bac068e0de7857" deployed. + +Token address: hash-565bd0bde39c8c3dd79e49c037e05eac8add2b2193e86a91a6bac068e0de7857 + +πŸ’ INFO : Calling "hash-565bd0bde39c8c3dd79e49c037e05eac8add2b2193e86a91a6bac068e0de7857" with entrypoint "propose_new_mint". +πŸ™„ WAIT : Waiting 15s for "2f89cc96b6f8f05b88f8e75bef3a2f0ba39e9ab761693afff49e4112aa9d7361". +πŸ™„ WAIT : Waiting 15s for "2f89cc96b6f8f05b88f8e75bef3a2f0ba39e9ab761693afff49e4112aa9d7361". +πŸ’ INFO : Deploy "2f89cc96b6f8f05b88f8e75bef3a2f0ba39e9ab761693afff49e4112aa9d7361" successfully executed. +πŸ’ INFO : Calling "hash-565bd0bde39c8c3dd79e49c037e05eac8add2b2193e86a91a6bac068e0de7857" with entrypoint "vote". +πŸ™„ WAIT : Waiting 15s for "aca9ae847cfcb97c81b4c64992515ff14d6f63a60f7c141558463f5b752058a5". +πŸ™„ WAIT : Waiting 15s for "aca9ae847cfcb97c81b4c64992515ff14d6f63a60f7c141558463f5b752058a5". +πŸ’ INFO : Deploy "aca9ae847cfcb97c81b4c64992515ff14d6f63a60f7c141558463f5b752058a5" successfully executed. +πŸ’ INFO : advance_block_time called - Waiting for 660000 ms +πŸ’ INFO : Calling "hash-565bd0bde39c8c3dd79e49c037e05eac8add2b2193e86a91a6bac068e0de7857" with entrypoint "tally". +πŸ™„ WAIT : Waiting 15s for "223b135edbeadd88425183abaec0b0afb7d7770ffc57eba9054e3ea60e9e9cef". +πŸ™„ WAIT : Waiting 15s for "223b135edbeadd88425183abaec0b0afb7d7770ffc57eba9054e3ea60e9e9cef". +πŸ’ INFO : Deploy "223b135edbeadd88425183abaec0b0afb7d7770ffc57eba9054e3ea60e9e9cef" successfully executed. + +Owner's balance: 1000 +Tutorial creator's balance: 1000 +``` + +Congratulations, your contract is now deployed on the Casper network! +Before we move on, note the address of the token! + +We will use it in the next section to interact with the token. In our case it is +`hash-565bd0bde39c8c3dd79e49c037e05eac8add2b2193e86a91a6bac068e0de7857`. + +## Cspr.live + +The first thing we will do is to explore Casper's network block explorer, +[cspr.live](https://cspr.live/). We can put the address of our token in the search bar +to find it. + +:::note +If you deployed your contract on the Testnet, remember to make sure that the Testnet +network is selected in the dropdown menu in the top right corner. +::: + +If everything is set up correctly, you should see the contract package's details. +Besides the owner, keys etc., you can also see the contract's metdata, if it +was developed using a standard that cspr.live supports. + +Indeed, we can see that it detected that our contract is a CEP-18 token! +We see the name, symbol and total supply. +All the mentions of the contract on the website will use the token name instead +of the contract address. + +![contract.png](../contract.png) + +Additionally, on the Token Txs tab, we can see the transactions that happened +with the token. We can see the minting transaction we did in the previous section +and transfers done during the voting process. + +![transactions.png](../transactions.png) + +If we click on one of the accounts that recieved the tokens, we will go to the +account page. Here, on the Tokens tab, we can see all the tokens that the account +has - and OurToken is one of them! + +If you wish, you can check the status of the contract deployed during the development +of this tutorial [here](https://testnet.cspr.live/contract-package/565bd0bde39c8c3dd79e49c037e05eac8add2b2193e86a91a6bac068e0de7857). + +## Transferring Tokens using Casper Wallet + +Casper wallet can do much more than just logging in to the faucet, exporting +the private keys and transferring CSPR. It can also interact with the contracts +deployed on the network. + +If you deployed the contract and left some OT tokens to yourself, you should see +them in the Casper Wallet window. + +You should also be able to transfer them to another account! + +![wallet.png](../wallet.png) + +## Conclusion + +We've successfully deployed a token on the Casper network and interacted with it +using the Odra backend and Casper Wallet. We've also learned how to use the +cspr.live block explorer to check the status of your contract. + +Odra, Cspr.live and Casper Wallet are just a few of the tools that the Casper ecosystem +provides. Feel free to explore them on [casperecosystem.io](https://casperecosystem.io/). + diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/erc20.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/erc20.md new file mode 100644 index 000000000..d82c000f4 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/erc20.md @@ -0,0 +1,374 @@ +--- +sidebar_position: 3 +--- + +# ERC-20 +It's time for something that every smart contract developer has done at least once. Let's try to implement [Erc20][erc20] standard. Of course, we are going to use the Odra Framework. + +The ERC-20 standard establishes a uniform specification for fungible tokens. This implies that each token shares attributes that make it indistinguishable from another token of the same type and value. + +## Framework features +A module we will write in a minute, will help you master a few Odra features: + +* Advanced storage using key-value pairs, +* Odra types such as `Address`, +* Advanced event assertion. + +## Code + +Our module features a considerably more complex storage layout compared to the previous example. + +It is designed to store the following data: +1. Immutable metadata - name, symbol, and decimals. +2. Total supply. +3. Balances of individual users. +4. Allowances, essentially indicating who is permitted to spend tokens on behalf of another user. + +## Module definition +```rust title=erc20.rs showLineNumbers +use odra::prelude::*; +use odra::{Address, casper_types::U256, Mapping, Var}; + +#[odra::module(events = [Transfer, Approval])] +pub struct Erc20 { + decimals: Var, + symbol: Var, + name: Var, + total_supply: Var, + balances: Mapping, + allowances: Mapping<(Address, Address), U256> +} +``` + +* **L10** - For the first time, we need to store key-value pairs. In order to do that, we use `Mapping`. The name is taken after Solidity's native type `mapping`. +* **L11** - Odra does not allows nested `Mapping`s as Solidity does. Instead, you can create a compound key using a tuple of keys. + +### Metadata + +```rust title=erc20.rs showLineNumbers +#[odra::module] +impl Erc20 { + pub fn init(&mut self, name: String, symbol: String, decimals: u8, initial_supply: U256) { + let caller = self.env().caller(); + self.name.set(name); + self.symbol.set(symbol); + self.decimals.set(decimals); + self.mint(&caller, &initial_supply); + } + + pub fn name(&self) -> String { + self.name.get_or_default() + } + + pub fn symbol(&self) -> String { + self.symbol.get_or_default() + } + + pub fn decimals(&self) -> u8 { + self.decimals.get_or_default() + } + + pub fn total_supply(&self) -> U256 { + self.total_supply.get_or_default() + } +} + +impl Erc20 { + pub fn mint(&mut self, address: &Address, amount: &U256) { + self.balances.add(address, *amount); + self.total_supply.add(*amount); + + self.env().emit_event(Transfer { + from: None, + to: Some(*address), + amount: *amount + }); + } +} + +#[odra::event] +pub struct Transfer { + pub from: Option
, + pub to: Option
, + pub amount: U256 +} +``` + +* **L1** - The first `impl` block, marked as a module, contains functions defined in the ERC-20 standard. +* **L3-L9** - A constructor sets the token metadata and mints the initial supply. +* **L28** - The second `impl` is not an Odra module; in other words, these functions will not be part of the contract's public interface. +* **L29-L38** - The `mint` function is public, so, like in regular Rust code, it will be accessible from the outside. `mint()` uses the notation `self.balances.add(address, *amount);`, which is syntactic sugar for: +```rust +use odra::UnwrapOrRevert; + +let current_balance = self.balances.get(address).unwrap_or_default(); +let new_balance = ::overflowing_add(current_balance, current_balance).unwrap_or_revert(&self.env()); +self.balances.set(address, new_balance); +``` + +### Core + +To ensure comprehensive functionality, let's implement the remaining features such as `transfer`, `transfer_from`, and `approve`. Since they do not introduce any new concepts, we will present them without additional remarks. + +```rust showLineNumbers title=erc20.rs +#[odra::module] +impl Erc20 { + ... + + pub fn transfer(&mut self, recipient: &Address, amount: &U256) { + let caller = self.env().caller(); + self.raw_transfer(&caller, recipient, amount); + } + + pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + let spender = self.env().caller(); + self.spend_allowance(owner, &spender, amount); + self.raw_transfer(owner, recipient, amount); + } + + pub fn approve(&mut self, spender: &Address, amount: &U256) { + let owner = self.env().caller(); + self.allowances.set(&(owner, *spender), *amount); + self.env().emit_event(Approval { + owner, + spender: *spender, + value: *amount + }); + } + + pub fn balance_of(&self, address: &Address) -> U256 { + self.balances.get_or_default(&address) + } + + pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 { + self.allowances.get_or_default(&(*owner, *spender)) + } +} + +impl Erc20 { + ... + + fn raw_transfer(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + let owner_balance = self.balances.get_or_default(&owner); + if *amount > owner_balance { + self.env().revert(Error::InsufficientBalance) + } + self.balances.set(owner, owner_balance - *amount); + self.balances.add(recipient, *amount); + self.env().emit_event(Transfer { + from: Some(*owner), + to: Some(*recipient), + amount: *amount + }); + } + + fn spend_allowance(&mut self, owner: &Address, spender: &Address, amount: &U256) { + let allowance = self.allowance(owner, spender); + if allowance < *amount { + self.env().revert(Error::InsufficientAllowance) + } + let new_allowance = allowance - *amount; + self.allowances + .set(&(*owner, *spender), new_allowance); + self.env().emit_event(Approval { + owner: *owner, + spender: *spender, + value: allowance - *amount + }); + } +} + +#[odra::event] +pub struct Approval { + pub owner: Address, + pub spender: Address, + pub value: U256 +} + +#[odra::odra_error] +pub enum Error { + InsufficientBalance = 1, + InsufficientAllowance = 2, +} +``` + +Now, compare the code we have written, with [Open Zeppelin code][erc20-open-zeppelin]. Out of 10, how Solidity-ish is our implementation? + +### Test + +```rust title=erc20.rs showLineNumbers +#[cfg(test)] +pub mod tests { + use super::*; + use odra::{casper_types::U256, host::{Deployer, HostEnv, HostRef}}; + + const NAME: &str = "Plascoin"; + const SYMBOL: &str = "PLS"; + const DECIMALS: u8 = 10; + const INITIAL_SUPPLY: u32 = 10_000; + + fn setup() -> (HostEnv, Erc20HostRef) { + let env = odra_test::env(); + ( + env.clone(), + Erc20HostRef::deploy( + &env, + Erc20InitArgs { + symbol: SYMBOL.to_string(), + name: NAME.to_string(), + decimals: DECIMALS, + initial_supply: INITIAL_SUPPLY.into() + } + ) + ) + } + + #[test] + fn initialization() { + // When deploy a contract with the initial supply. + let (env, erc20) = setup(); + + // Then the contract has the metadata set. + assert_eq!(erc20.symbol(), SYMBOL.to_string()); + assert_eq!(erc20.name(), NAME.to_string()); + assert_eq!(erc20.decimals(), DECIMALS); + + // Then the total supply is updated. + assert_eq!(erc20.total_supply(), INITIAL_SUPPLY.into()); + + // Then a Transfer event was emitted. + assert!(env.emitted_event( + &erc20, + &Transfer { + from: None, + to: Some(env.get_account(0)), + amount: INITIAL_SUPPLY.into() + } + )); + } + + #[test] + fn transfer_works() { + // Given a new contract. + let (env, mut erc20) = setup(); + + // When transfer tokens to a recipient. + let sender = env.get_account(0); + let recipient = env.get_account(1); + let amount = 1_000.into(); + erc20.transfer(&recipient, &amount); + + // Then the sender balance is deducted. + assert_eq!( + erc20.balance_of(&sender), + U256::from(INITIAL_SUPPLY) - amount + ); + + // Then the recipient balance is updated. + assert_eq!(erc20.balance_of(&recipient), amount); + + // Then Transfer event was emitted. + assert!(env.emitted_event( + &erc20, + &Transfer { + from: Some(sender), + to: Some(recipient), + amount + } + )); + } + + #[test] + fn transfer_error() { + // Given a new contract. + let (env, mut erc20) = setup(); + + // When the transfer amount exceeds the sender balance. + let recipient = env.get_account(1); + let amount = U256::from(INITIAL_SUPPLY) + U256::one(); + + // Then an error occurs. + assert!(erc20.try_transfer(&recipient, &amount).is_err()); + } + + #[test] + fn transfer_from_and_approval_work() { + let (env, mut erc20) = setup(); + + let (owner, recipient, spender) = + (env.get_account(0), env.get_account(1), env.get_account(2)); + let approved_amount = 3_000.into(); + let transfer_amount = 1_000.into(); + + assert_eq!(erc20.balance_of(&owner), U256::from(INITIAL_SUPPLY)); + + // Owner approves Spender. + erc20.approve(&spender, &approved_amount); + + // Allowance was recorded. + assert_eq!(erc20.allowance(&owner, &spender), approved_amount); + assert!(env.emitted_event( + &erc20, + &Approval { + owner, + spender, + value: approved_amount + } + )); + + // Spender transfers tokens from Owner to Recipient. + env.set_caller(spender); + erc20.transfer_from(&owner, &recipient, &transfer_amount); + + // Tokens are transferred and allowance decremented. + assert_eq!( + erc20.balance_of(&owner), + U256::from(INITIAL_SUPPLY) - transfer_amount + ); + assert_eq!(erc20.balance_of(&recipient), transfer_amount); + assert!(env.emitted_event( + &erc20, + &Approval { + owner, + spender, + value: approved_amount - transfer_amount + } + )); + assert!(env.emitted_event( + &erc20, + &Transfer { + from: Some(owner), + to: Some(recipient), + amount: transfer_amount + } + )); + // assert!(env.emitted(erc20.address(), "Transfer")); + } + + #[test] + fn transfer_from_error() { + // Given a new instance. + let (env, mut erc20) = setup(); + + // When the spender's allowance is zero. + let (owner, spender, recipient) = + (env.get_account(0), env.get_account(1), env.get_account(2)); + let amount = 1_000.into(); + env.set_caller(spender); + + // Then transfer fails. + assert_eq!( + erc20.try_transfer_from(&owner, &recipient, &amount), + Err(Error::InsufficientAllowance.into()) + ); + } +} +``` + +* **L146** - Alternatively, if you don't want to check the entire event, you may assert only its type. + +## What's next +Having two modules: `Ownable` and `Erc20`, let's combine them, and create an ERC-20 on steroids. + +[erc20]: https://eips.ethereum.org/EIPS/eip-20 +[erc20-open-zeppelin]: https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/nft.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/nft.md new file mode 100644 index 000000000..4509b39f9 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/nft.md @@ -0,0 +1,475 @@ +--- +sidebar_position: 6 +--- + +# Ticketing System +Non-fungible tokens (NFTs) are digital assets that represent ownership of unique items or pieces of content. They are commonly used for digital art, collectibles, in-game items, and other unique assets. In this tutorial, we will create a simple ticketing system based on NFT tokens. + +Our contract will adhere to the CEP-78 standard, which is the standard for NFTs on the Casper blockchain. + +Learn more about the CEP-78 standard [here](https://github.com/casper-ecosystem/cep-78-enhanced-nft/tree/dev/docs). + +### Ticket Office Contract + +Our TicketOffice contract will include the following features: +* Compliance with the CEP-78 standard. +* Ownership functionality. +* Only the owner can issue new event tickets. +* Users can purchase tickets for events. +* Tickets are limited to a one-time sale. +* Public access to view the total income of the `TicketOffice`. + +### Setup the project + +Creating a new NFT token with Odra is straightforward. Use the `cargo odra new` command to create a new project with the CEP-78 template: + +```bash +cargo odra new --name ticket-office --template cep78 +``` + +### Contract implementation + +Let's start implementing the `TicketOffice` contract by modify the code generated from the template. + +```rust showLineNumbers title="src/token.rs" +use odra::{ + args::Maybe, casper_types::U512, prelude::*, Address, Mapping, SubModule, UnwrapOrRevert +}; +use odra_modules::access::Ownable; +use odra_modules::cep78::{ + modalities::{MetadataMutability, NFTIdentifierMode, NFTKind, NFTMetadataKind, OwnershipMode}, + token::Cep78, +}; + +pub type TicketId = u64; + +#[odra::odra_type] +pub enum TicketStatus { + Available, + Sold, +} + +#[odra::odra_type] +pub struct TicketInfo { + event_name: String, + price: U512, + status: TicketStatus, +} + +#[odra::event] +pub struct OnTicketIssue { + ticket_id: TicketId, + event_name: String, + price: U512, +} + +#[odra::event] +pub struct OnTicketSell { + ticket_id: TicketId, + buyer: Address, +} + +#[odra::odra_error] +pub enum Error { + TicketNotAvailableForSale = 200, + InsufficientFunds = 201, + InvalidTicketId = 202, + TicketDoesNotExist = 203, +} + +#[odra::module( + events = [OnTicketIssue, OnTicketSell], + errors = Error +)] +pub struct TicketOffice { + token: SubModule, + ownable: SubModule, + tickets: Mapping, +} + +#[odra::module] +impl TicketOffice { + pub fn init(&mut self, collection_name: String, collection_symbol: String, total_supply: u64) { + self.ownable.init(); + let receipt_name = format!("cep78_{}", collection_name); + self.token.init( + collection_name, + collection_symbol, + total_supply, + OwnershipMode::Transferable, + NFTKind::Digital, + NFTIdentifierMode::Ordinal, + NFTMetadataKind::Raw, + MetadataMutability::Immutable, + receipt_name, + // remaining args are optional and can set to Maybe::None + ... + ); + } + + pub fn issue_ticket(&mut self, event_name: String, price: U512) { + let env = self.env(); + let caller = env.caller(); + self.ownable.assert_owner(&caller); + // mint a new token + let (_, _, token_id) = self.token.mint(caller, "".to_string(), Maybe::None); + let ticket_id: u64 = token_id + .parse() + .map_err(|_| Error::InvalidTicketId) + .unwrap_or_revert(&env); + // store ticket info + self.tickets.set( + &ticket_id, + TicketInfo { + event_name: event_name.clone(), + price, + status: TicketStatus::Available, + }, + ); + // emit an event + env.emit_event(OnTicketIssue { + ticket_id, + event_name, + price, + }); + } + + #[odra(payable)] + pub fn buy_ticket(&mut self, ticket_id: TicketId) { + let env = self.env(); + let owner = self.ownable.get_owner(); + let buyer = env.caller(); + let value = env.attached_value(); + // only tokens owned by the owner can be sold + if self.token.owner_of(Maybe::Some(ticket_id), Maybe::None) != owner { + env.revert(Error::TicketNotAvailableForSale); + } + let mut ticket = self + .tickets + .get(&ticket_id) + .unwrap_or_revert_with(&env, Error::TicketDoesNotExist); + // only available tickets can be sold + if ticket.status != TicketStatus::Available { + env.revert(Error::TicketNotAvailableForSale); + } + // check if the buyer sends enough funds + if value < ticket.price { + env.revert(Error::InsufficientFunds); + } + // transfer csprs to the owner + env.transfer_tokens(&owner, &value); + // transfer the ticket to the buyer + self.token + .transfer(Maybe::Some(ticket_id), Maybe::None, owner, buyer); + ticket.status = TicketStatus::Sold; + self.tickets.set(&ticket_id, ticket); + + env.emit_event(OnTicketSell { ticket_id, buyer }); + } + + pub fn balance_of(&self) -> U512 { + self.env().self_balance() + } +} +``` +* **L10-L44** - We define structures and enums that will be used in our contract. `TicketStatus` enum represents the status of a ticket, `TicketInfo` struct contains information about a ticket that is written to the storage, `TicketId` is a type alias for `u64`. `OnTicketIssue` and `OnTicketSell` are events that will be emitted when a ticket is issued or sold. +* **L46-L49** - Register errors and events that will be used in our contract, required to produce a complete contract schema. +* **L51-L53** - `TicketOffice` module definition. The module contains a `Cep78` token, an `Ownable` module, and a `Mapping` that stores information about tickets. +* **L58-L74** - The `init` function has been generated from the template and there is no need to modify it, except the `Ownable` module initialization. +* **L76-L94** - The `issue_ticket` function allows the owner to issue a new ticket. The function mints a new token, stores information about the ticket, and emits an `OnTicketIssue` event. +* **L103** - The `payable` attribute indicates that the `buy_ticket` function can receive funds. +* **L104-L134** - The `buy_ticket` function checks if the ticket is available for sale, if the buyer sends enough funds, and transfers the ticket to the buyer. Finally, the function updates the ticket status and emits an `OnTicketSell` event. + +Lets test the contract. The test scenario will be as follows: +1. Deploy the contract. +2. Issue two tickets. +3. Try to buy a ticket with insufficient funds. +4. Buy tickets. +5. Try to buy the same ticket again. +6. Check the balance of the contract. + +```rust showLineNumbers title="src/tests.rs" +use odra::{ + casper_types::U512, + host::{Deployer, HostRef}, +}; + +use crate::token::{Error, TicketOfficeHostRef, TicketOfficeInitArgs}; + +#[test] +fn it_works() { + let env = odra_test::env(); + let init_args = TicketOfficeInitArgs { + collection_name: "Ticket".to_string(), + collection_symbol: "T".to_string(), + total_supply: 100, + }; + let mut contract = TicketOfficeHostRef::deploy(&env, init_args); + contract.issue_ticket("Ev".to_string(), U512::from(100)); + contract.issue_ticket("Ev".to_string(), U512::from(50)); + + let buyer = env.get_account(1); + env.set_caller(buyer); + + assert_eq!( + contract + .with_tokens(U512::from(50)) + .try_buy_ticket(0), + Err(Error::InsufficientFunds.into()) + ); + + assert_eq!( + contract + .with_tokens(U512::from(100)) + .try_buy_ticket(0), + Ok(()) + ); + assert_eq!( + contract + .with_tokens(U512::from(50)) + .try_buy_ticket(1), + Ok(()) + ); + + assert_eq!( + contract + .with_tokens(U512::from(100)) + .try_buy_ticket(0), + Err(Error::TicketNotAvailableForSale.into()) + ); +} +``` + +Unfortunately, the test failed. The first assertion succeeds because the buyer sends insufficient funds to buy the ticket. However, the second assertion fails even though the buyer sends enough funds to purchase the ticket. The buy_ticket function reverts with `Cep78Error::InvalidTokenOwner` because the buyer attempts to transfer a token that they do not own, are not approved for, or are not an operator of. + +```rust title="odra/modules/src/cep78/token78.rs" +pub fn transfer( + &mut self, + token_id: Maybe, + token_hash: Maybe, + source_key: Address, + target_key: Address +) -> TransferReceipt { + ... + + if !is_owner && !is_approved && !is_operator { + self.revert(CEP78Error::InvalidTokenOwner); + } + + ... +} +``` + +Let's fix it by redesigning our little system. + +### Redesign + +Since a buyer cannot purchase a ticket directly, we need to introduce an intermediary β€” an operator who will be responsible for buying tickets on behalf of the buyer. The operator will be approved by the ticket office to transfer tickets. + +The sequence diagram below illustrates the new flow: + +```mermaid +sequenceDiagram; + autonumber + actor Owner + Owner->>+TicketOffice: Deploy + Owner->>+Operator: Deploy + actor Buyer + Owner->>TicketOffice: call register_operator + TicketOffice->>Operator: Register + Operator->>TicketOffice: Register + Owner->>TicketOffice: call issue_ticket + TicketOffice->>Operator: Approve + Buyer->>Operator: call buy_ticket + Operator->>TicketOffice: call buy_ticket + TicketOffice->>Buyer: Transfer ticket +``` +#### Ticket Operator Contract + +As shown in the sequence diagram, a new contract will act as an operator for the ticket office. To create this new contract, use the `cargo odra generate` command. + +```sh +cargo odra generate -c ticket_operator +``` + +```rust showLineNumbers title="src/ticket_operator.rs" +use crate::token::{TicketId, TicketOfficeContractRef}; +use odra::{casper_types::U512, prelude::*, Address, UnwrapOrRevert, Var}; + +#[odra::odra_error] +pub enum Error { + UnknownTicketOffice = 300, +} + +#[odra::module(errors = Error)] +pub struct TicketOperator { + ticket_office_address: Var
, +} + +#[odra::module] +impl TicketOperator { + pub fn register(&mut self, ticket_office_address: Address) { + self.ticket_office_address.set(ticket_office_address); + } + + // now the operator's `buy_ticket` receives funds. + #[odra(payable)] + pub fn buy_ticket(&mut self, ticket_id: TicketId) { + let env = self.env(); + let buyer = env.caller(); + let value = env.attached_value(); + let center = self + .ticket_office_address + .get() + .unwrap_or_revert_with(&env, Error::UnknownTicketOffice); + let mut ticket_contract = TicketOfficeContractRef::new(env, center); + // now and approved entity - the operator - buys the ticket on behalf of the buyer + ticket_contract.buy_ticket(ticket_id, buyer, value); + } + + pub fn balance_of(&self) -> U512 { + self.env().self_balance() + } +} +``` + +* **L4-L7** - Define errors that will be used in the contract. +* **L9-L13** - Define the `TicketOperator` module that stores the address of the ticketing office. +* **L16-L18** - The `register` function sets the address of the ticketing office. +* **L20-L32** - The `buy_ticket` function buys a ticket on behalf of the buyer using the ticket office address. The function forwards the call to the ticketing office contract. We simply create a `TicketOfficeContractRef` to interact we the `TicketOffice` contract. Note that, the operator's `buy_ticket` now receives funds. + + +Now we need to adjust the `TicketOffice` contract to use the `TicketOperator` contract to buy tickets. + +```rust showLineNumbers title="src/token.rs" +use odra::Var; + +... + +#[odra::odra_error] +pub enum Error { + ... + MissingOperator = 204, + Unauthorized = 205, +} + +#[odra::module] +pub struct TicketOffice { + ... + operator: Var
, +} + +#[odra::module] +impl TicketOffice { + ... + + pub fn register_operator(&mut self, operator: Address) { + // only the owner can register an operator + let caller = self.env().caller(); + self.ownable.assert_owner(&caller); + // store the ticketing center address in the operator contract + TicketOperatorContractRef::new(self.env(), operator).register(self.env().self_address()); + self.operator.set(operator); + } + + pub fn issue_ticket(&mut self, event_name: String, price: U512) { + // minting logic remains the same... + ... + + // approve the operator to transfer the ticket + let operator = self.operator(); + self.token + .approve(operator, Maybe::Some(ticket_id), Maybe::None); + + // emit an event + ... + } + + pub fn buy_ticket(&mut self, ticket_id: TicketId, buyer: Address, value: U512) { + let env = self.env(); + let owner = self.ownable.get_owner(); + let caller = env.caller(); + // make sure the caller is the operator + if !self.is_operator(caller) { + env.revert(Error::Unauthorized); + } + + ... + // the logic remains the same, except for the csprs transfer + // it is now handled by the operator contract. + // env.transfer_tokens(&owner, &value); + } + + #[inline] + fn is_operator(&self, caller: Address) -> bool { + Some(caller) == self.operator.get() + } + + #[inline] + fn operator(&self) -> Address { + self.operator + .get() + .unwrap_or_revert_with(&self.env(), Error::MissingOperator) + } +} +``` +* **L15** - the contract stores the operator address. +* **L22-L29** - a new function `register_operator` allows the owner to register an operator. Also calls the `register` entry point on the operator contract. +* **L36-38** - modify the `issue_ticket` function: once a new token is minted, approves the operator to transfer the ticket later. +* **L44-L57** - modify the `buy_ticket` function: check if the caller is the operator, do not transfer cspr to the contract - now the operator collect funds. +* We also added two helper functions: `is_operator` and `operator` to check if the caller is the operator and get the operator address. Two new errors were added: `MissingOperator` and `Unauthorized`. + +Now we need to update our tests to create a scenario we presented in the sequence diagram. + +```rust showLineNumbers title="src/tests.rs" +use odra::{ + casper_types::U512, + host::{Deployer, HostRef, NoArgs}, + OdraResult, +}; + +use crate::{ + ticket_operator::TicketOperatorHostRef, + token::{Error, TicketId, TicketOfficeContractRef, TicketOfficeInitArgs}, +}; + +#[test] +fn it_works() { + let env = odra_test::env(); + let init_args = TicketOfficeInitArgs { + collection_name: "Ticket".to_string(), + collection_symbol: "T".to_string(), + total_supply: 100, + }; + let operator = TicketOperatorHostRef::deploy(&env, NoArgs); + let mut ticket_office = TicketOfficeContractRef::deploy(&env, init_args); + ticket_office.register_operator(operator.address().clone()); + ticket_office.issue_ticket("Ev".to_string(), U512::from(100)); + ticket_office.issue_ticket("Ev".to_string(), U512::from(50)); + + let buyer = env.get_account(1); + env.set_caller(buyer); + + assert_eq!( + buy_ticket(&operator, 0, 50), + Err(Error::InsufficientFunds.into()) + ); + assert_eq!(buy_ticket(&operator, 0, 100), Ok(())); + assert_eq!(buy_ticket(&operator, 1, 50), Ok(())); + assert_eq!( + buy_ticket(&operator, 0, 100), + Err(Error::TicketNotAvailableForSale.into()) + ); + + assert_eq!(operator.balance_of(), U512::from(150)); +} + +fn buy_ticket(operator: &TicketOperatorHostRef, id: TicketId, price: u64) -> OdraResult<()> { + operator.with_tokens(U512::from(price)).try_buy_ticket(id) +} + +``` + +### Conclusion + +In this tutorial, we created a simple ticketing system using the CEP-78 standard. This guide demonstrates how to combine various Odra features, including modules, events, errors, payable functions, and cross-contract calls. \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/odra-sol.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/odra-sol.md new file mode 100644 index 000000000..fba4342a3 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/odra-sol.md @@ -0,0 +1,1591 @@ +--- +sidebar_position: 9 +slug: odra-solidity +image: "/img/odra-sol.png" +description: Learn how to transition from Solidity to Odra. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Odra for Solidity developers + +## Introduction + +Hi, stranger Solidity developer! If you are looking to expand your horizons into Rust-based smart contract development, you've come to the right place. Odra is a high-level framework designed to simplify the development of smart contracts for the Casper Network. This tutorial will guide you through the basics of transitioning from Solidity to Odra, highlighting key differences and providing practical examples. Before we delve into the details, we have great news for you. From the very beginning, we have been thinking of you. Our main goal was to design the framework in a way that flattens the learning curve, especially for Solidity developers. + +## Prerequisites +To follow this guide, you should have: + +* Knowledge of Solidity. +* Familiarity with Ethereum and smart contract concepts. +* Basic understanding of Rust, as Odra is based on it. + +## Hello World + +Let's start with a simple "Hello World" contract in Odra. The following code snippet demonstrates a basic smart contract that stores a greeting message. + + + + +```rust showLineNumbers +use odra::{prelude::*, Var}; + +#[odra::module] +pub struct HelloWorld { + greet: Var, +} + +#[odra::module] +impl HelloWorld { + pub fn init(&mut self, message: String) { + self.greet.set(message); + } + + pub fn get(&self) -> String { + self.greet.get_or_default() + } +} +``` + + + + +```sol showLineNumbers +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract HelloWorld { + string public greet = "Hello World!"; +} +``` + + + +As you may have noticed, the Odra code is slightly more verbose than the Solidity code. To define a contract in Odra, you need to create a struct and implement a module for it, both annotated with the `odra::module` attribute. The struct contains the contract's state variables, while the module defines the contract's functions. In this example, the `HelloWorld` struct has a single state variable greet, which stores the greeting message. The module contains two functions: `init` to `set` the greeting message and get to retrieve it. +Two key differences are: +1. Odra does not generate getters for public state variables automatically, so you need to define them explicitly. +2. To initialize values, you must do it in the `init` function, which is the contract constructor. You can't assign defaults outside the constructor. + +## Variable Storage and State Management + +### Data Types + + + + +```rust showLineNumbers +use core::str::FromStr; +use odra::{ + casper_types::{bytesrepr::Bytes, U256}, + module::Module, + prelude::*, + Address, UnwrapOrRevert, Var, +}; + +#[odra::module] +pub struct Primitives { + boo: Var, + u: Var, // u8 is the smallest unsigned integer type + u2: Var, // U256 is the biggest unsigned integer type + i: Var, // i32 is the smallest signed integer type + i2: Var, // i64 is the biggest signed integer type + address: Var
, + bytes: Var, + default_boo: Var, + default_uint: Var, + default_int: Var, + default_addr: Var
, +} + +#[odra::module] +impl Primitives { + pub fn init(&mut self) { + self.boo.set(true); + self.u.set(1); + self.u2.set(U256::from(456)); + self.i.set(-1); + self.i2.set(456); + self.address.set( + Address::from_str( + "hash-d4b8fa492d55ac7a515c0c6043d72ba43c49cd120e7ba7eec8c0a330dedab3fb", + ) + .unwrap_or_revert(&self.env()), + ); + self.bytes.set(Bytes::from(vec![0xb5])); + + let _min_int = U256::zero(); + let _max_int = U256::MAX; + } + + // For the types that have default values, we can use the get_or_default method + pub fn get_default_boo(&self) -> bool { + self.default_boo.get_or_default() + } + + pub fn get_default_uint(&self) -> U256 { + self.default_uint.get_or_default() + } + + pub fn get_default_int(&self) -> i64 { + self.default_int.get_or_default() + } + + // Does not compile - Address does not have the default value + pub fn get_default_addr(&self) -> Address { + self.default_addr.get_or_default() + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/primitives/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Primitives { + bool public boo = true; + + uint8 public u8 = 1; + uint256 public u256 = 456; + + int8 public i8 = -1; + int256 public i256 = 456; + + int256 public minInt = type(int256).min; + int256 public maxInt = type(int256).max; + + address public addr = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c; + bytes1 a = 0xb5; // [10110101] + + // Default values + // Unassigned variables have a default value + bool public defaultBoo; // false + uint256 public defaultUint; // 0 + int256 public defaultInt; // 0 + address public defaultAddr; // 0x0000000000000000000000000000000000000000 +} +``` + + + +The range of integer types in Odra is slightly different from Solidity. Odra provides a wide range of integer types: `u8`, `u16`, `u32`, `u64`, `U128`, and `U256` for unsigned integers, and `i32` and `i64` for signed integers. + +The `Address` type in Odra is used to represent account and contract addresses. In Odra, there is no default/zero value for the `Address` type; the workaround is to use `Option
`. + +The `Bytes` type is used to store byte arrays. + +Values are stored in units called `Named Keys` and `Dictionaries`. Additionally, local variables are available within the entry points and can be used to perform necessary actions or computations within the scope of each entry point. + +### Constants and Immutability + + + + +```rust showLineNumbers +use odra::{casper_types::{account::AccountHash, U256}, Address}; + +#[odra::module] +pub struct Constants; + +#[odra::module] +impl Constants { + pub const MY_UINT: U256 = U256([123, 0, 0, 0]); + pub const MY_ADDRESS: Address = Address::Account( + AccountHash([0u8; 32]) + ); +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/constants/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Constants { + // coding convention to uppercase constant variables + address public constant MY_ADDRESS = + 0x777788889999AaAAbBbbCcccddDdeeeEfFFfCcCc; + uint256 public constant MY_UINT = 123; +} +``` + + + +In Odra, you can define constants using the `const` keyword. Constants are immutable and can be of any type, including custom types. In addition to constants, Solidity also supports the `immutable` keyword, which is used to set the value of a variable once, in the constructor. Further attempts to alter this value result in a compile error. Odra/Rust does not have an equivalent to Solidity's `immutable` keyword. + +### Variables + + + + +```rust showLineNumbers +use odra::{casper_types::U256, prelude::*, Var}; + +#[odra::module] +pub struct Variables { + text: Var, + my_uint: Var, +} + +#[odra::module] +impl Variables { + pub fn init(&mut self) { + self.text.set("Hello".to_string()); + self.my_uint.set(U256::from(123)); + } + + pub fn do_something(&self) { + // Local variables + let i = 456; + // Env variables + let timestamp = self.env().get_block_time(); + let sender = self.env().caller(); + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/variables/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Variables { + // State variables are stored on the blockchain. + string public text = "Hello"; + uint256 public num = 123; + + function doSomething() public { + // Local variables are not saved to the blockchain. + uint256 i = 456; + + // Here are some global variables + uint256 timestamp = block.timestamp; // Current block timestamp + address sender = msg.sender; // address of the caller + } +} +``` + + + +In Solidity there are three types of variables: state variables, local variables, and global variables. State variables are stored on the blockchain and are accessible by all functions within the contract. Local variables are not stored on the blockchain and are only available within the function in which they are declared. Global variables provide information about the blockchain. Odra uses very similar concepts, but with some differences. In Odra, state variables are a part of a module definition, and local variables are available within the entry points and can be used to perform necessary actions or computations within the scope of each entry point. Global variables are accessed using an instance of `ContractEnv` retrieved using the `env()` function. + +### Arrays and Mappings + + + + +```rust showLineNumbers +use odra::{casper_types::U256, Address, Mapping}; + +#[odra::module] +pub struct MappingContract { + my_map: Mapping> +} + +#[odra::module] +impl MappingContract { + pub fn get(&self, addr: Address) -> U256 { + // self.my_map.get(&addr) would return Option> + // so we use get_or_default instead and unwrap the inner Option + self.my_map.get_or_default(&addr).unwrap_or_default() + } + + pub fn set(&mut self, addr: Address, i: U256) { + self.my_map.set(&addr, Some(i)); + } + + pub fn remove(&mut self, addr: Address) { + self.my_map.set(&addr, None); + } +} + +#[odra::module] +pub struct NestedMapping { + my_map: Mapping<(Address, U256), Option> +} + +#[odra::module] +impl NestedMapping { + pub fn get(&self, addr: Address, i: U256) -> bool { + self.my_map.get_or_default(&(addr, i)).unwrap_or_default() + } + + pub fn set(&mut self, addr: Address, i: U256, boo: bool) { + self.my_map.set(&(addr, i), Some(boo)); + } + + pub fn remove(&mut self, addr: Address, i: U256) { + self.my_map.set(&(addr, i), None); + } +} +``` + + + + +```sol showLineNumbers +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Mapping { + mapping(address => uint256) public myMap; + + function get(address _addr) public view returns (uint256) { + return myMap[_addr]; + } + + function set(address _addr, uint256 _i) public { + myMap[_addr] = _i; + } + + function remove(address _addr) public { + delete myMap[_addr]; + } +} + +contract NestedMapping { + mapping(address => mapping(uint256 => bool)) public nested; + + function get(address _addr1, uint256 _i) public view returns (bool) { + return nested[_addr1][_i]; + } + + function set(address _addr1, uint256 _i, bool _boo) public { + nested[_addr1][_i] = _boo; + } + + function remove(address _addr1, uint256 _i) public { + delete nested[_addr1][_i]; + } +} +``` + + + + + + + +```rust showLineNumbers +use odra::{prelude::*, Var}; + +#[odra::module] +pub struct Array { + // the size of the array must be known at compile time + arr: Var<[u8; 10]>, + vec: Var>, +} + +#[odra::module] +impl Array { + pub fn init(&mut self) { + self.arr.set([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]); + self.vec.set(vec![1, 2, 3, 4, 5]); + } + + pub fn get_arr(&self) -> [u8; 10] { + self.arr.get_or_default() + } + + pub fn push_vec(&mut self, value: u32) { + let mut vec = self.vec.get_or_default(); + vec.push(value); + self.vec.set(vec); + } + + pub fn pop_vec(&mut self) { + let mut vec = self.vec.get_or_default(); + vec.pop(); + self.vec.set(vec); + } + + pub fn update_arr(&mut self, index: u8, value: u8) { + let mut arr = self.arr.get_or_default(); + arr[index as usize] = value; + self.arr.set(arr); + } +} +``` + + + + +```sol showLineNumbers +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Array { + // Several ways to initialize an array + uint256[] public arr; + uint256[] public arr2 = [1, 2, 3]; + // Fixed sized array, all elements initialize to 0 + uint256[10] public myFixedSizeArr; + + function get(uint256 i) public view returns (uint256) { + return arr[i]; + } + + // Solidity can return the entire array. + // But this function should be avoided for + // arrays that can grow indefinitely in length. + function getArr() public view returns (uint256[] memory) { + return arr; + } + + function push(uint256 i) public { + // Append to array + // This will increase the array length by 1. + arr.push(i); + } + + function pop() public { + // Remove last element from array + // This will decrease the array length by 1 + arr.pop(); + } + + function getLength() public view returns (uint256) { + return arr.length; + } + + function remove(uint256 index) public { + // Delete does not change the array length. + // It resets the value at index to it's default value, + // in this case 0 + delete arr[index]; + } + + function examples() external { + // create array in memory, only fixed size can be created + uint256[] memory a = new uint256[](5); + } +} +``` + + + +For storing a collection of data as a single unit, Odra uses the Vec type for dynamic arrays and fixed-size arrays, both wrapped with the `Var` container. As in Solidity, you must be aware that reading the entire array in one go can be expensive, so it's better to avoid it for large arrays. In many cases, you can use a `Mapping` or `List` instead of an array or vector to store data. + +### Custom types + + + + +```rust showLineNumbers +use odra::{prelude::*, Var}; + +#[odra::odra_type] +#[derive(Default)] +pub enum Status { + #[default] + Pending, + Shipped, + Accepted, + Rejected, + Canceled, +} + +#[odra::module] +pub struct Enum { + status: Var, +} + +#[odra::module] +impl Enum { + pub fn get(&self) -> Status { + self.status.get_or_default() + } + + pub fn set(&mut self, status: Status) { + self.status.set(status); + } + + pub fn cancel(&mut self) { + self.status.set(Status::Canceled); + } + + pub fn reset(&mut self) { + self.status.set(Default::default()); + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/enum/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Enum { + // Enum representing shipping status + enum Status { + Pending, + Shipped, + Accepted, + Rejected, + Canceled + } + + // Default value is the first element listed in + // definition of the type, in this case "Pending" + Status public status; + + // Returns uint + // Pending - 0 + // Shipped - 1 + // Accepted - 2 + // Rejected - 3 + // Canceled - 4 + function get() public view returns (Status) { + return status; + } + + function set(Status _status) public { + status = _status; + } + + function cancel() public { + status = Status.Canceled; + } + + // delete resets the enum to its first value, 0 + function reset() public { + delete status; + } +} +``` + + + +In Odra, custom types are defined using the `#[odra::odra_type]` attribute. The enum can have a default value specified using the `#[default]` attribute if derived from the Default trait. The enum can be used as a state variable in a contract, and its value can be set and retrieved using the set and get functions. The value cannot be deleted; however, it can be set using the `Default::default()` function. + + + + +```rust showLineNumbers +use odra::{prelude::*, List}; + +#[odra::odra_type] +pub struct Todo { + text: String, + completed: bool, +} + +#[odra::module] +pub struct Enum { + // You could also use Var> instead of List, + // but List is more efficient for large arrays, + // it loads items lazily. + todos: List, +} + +#[odra::module] +impl Enum { + pub fn create(&mut self, text: String) { + self.todos.push(Todo { + text, + completed: false, + }); + } + + pub fn update_text(&mut self, index: u32, text: String) { + if let Some(mut todo) = self.todos.get(index) { + todo.text = text; + self.todos.replace(index, todo); + } + } + + pub fn toggle_complete(&mut self, index: u32) { + if let Some(mut todo) = self.todos.get(index) { + todo.completed = !todo.completed; + self.todos.replace(index, todo); + } + } + + // Odra does not create getters by default + pub fn get(&self, index: u32) -> Option { + self.todos.get(index) + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/structs/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Todos { + struct Todo { + string text; + bool completed; + } + + Todo[] public todos; + + function create(string calldata _text) public { + todos.push(Todo(_text, false)); + } + + // Solidity automatically created a getter for 'todos' so + // you don't actually need this function. + function get(uint256 _index) + public + view + returns (string memory text, bool completed) + { + Todo storage todo = todos[_index]; + return (todo.text, todo.completed); + } + + function updateText(uint256 _index, string calldata _text) public { + Todo storage todo = todos[_index]; + todo.text = _text; + } + + function toggleCompleted(uint256 _index) public { + Todo storage todo = todos[_index]; + todo.completed = !todo.completed; + } +} + +``` + + + +Similarly to enums, custom structs are defined using the `#[odra::odra_type]` attribute. The struct can be used to define a list of items in a contract. The list can be created using the `List` type, which is more efficient for large arrays as it loads items lazily. + +### Data Location + +In Solidity, data location is an important concept that determines where the data is stored and how it can be accessed. The data location can be `memory`, `storage`, or `calldata`. In Odra, data location is not explicitly defined, but whenever interacting with storage primitives (e.g., `Var`, `Mapping`, `List`), the data is stored in the contract's storage. + +## Functions + +Odra contracts define their entry point and internal functions within the impl block. Here's an example of a transfer function: + +```rust +impl Erc20 { + pub fn transfer(&mut self, recipient: &Address, amount: &U256) { + self.internal_transfer(&self.env().caller(), recipient, amount); + // Transfer logic goes here + } + + fn internal_transfer(&mut self, sender: &Address, recipient: &Address, amount: &U256) { + // Internal transfer logic goes here + } +} +``` +Functions can modify contract state and emit events using the [`ContractEnv`](../basics/06-communicating-with-host.md) function. + +### View and Pure + + + + +```rust showLineNumbers +use odra::Var; + +#[odra::module] +pub struct ViewAndPure { + x: Var +} + +#[odra::module] +impl ViewAndPure { + pub fn add_to_x(&self, y: u32) -> u32 { + self.x.get_or_default() + y + } +} + +pub fn add(i: u32, j: u32) -> u32 { + i + j +} +``` + + + + +```sol showLineNumbers +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract ViewAndPure { + uint256 public x = 1; + + // Promise not to modify the state. + function addToX(uint256 y) public view returns (uint256) { + return x + y; + } + + // Promise not to modify or read from the state. + function add(uint256 i, uint256 j) public pure returns (uint256) { + return i + j; + } +} +``` + + + +In Odra, you don't need to specify `view` or `pure` functions explicitly. All functions are considered `view` functions by default, meaning they can read contract state but not modify it. To modify the state, the first parameter (called the receiver parameter) should be `&mut self`. If you want to create a pure function that doesn't read or modify state, you can define it as a regular Rust function without any side effects. + +### Modifiers + + + + +```rust showLineNumbers +use odra::{module::Module, Var}; + +#[odra::module] +pub struct FunctionModifier { + x: Var, + locked: Var, +} + +#[odra::module] +impl FunctionModifier { + pub fn decrement(&mut self, i: u32) { + self.lock(); + self.x.set(self.x.get_or_default() - i); + + if i > 1 { + self.decrement(i - 1); + } + self.unlock(); + } + + #[inline] + fn lock(&mut self) { + if self.locked.get_or_default() { + self.env().revert(Error::NoReentrancy); + } + + self.locked.set(true); + } + + #[inline] + fn unlock(&mut self) { + self.locked.set(false); + } +} + +#[odra::odra_error] +pub enum Error { + NoReentrancy = 1, +} + +``` + + + + +```sol showLineNumbers +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract FunctionModifier { + uint256 public x = 10; + bool public locked; + + modifier noReentrancy() { + require(!locked, "No reentrancy"); + + locked = true; + _; + locked = false; + } + + function decrement(uint256 i) public noReentrancy { + x -= i; + + if (i > 1) { + decrement(i - 1); + } + } +} +``` + + + +In Odra, there is no direct equivalent to Solidity's function modifiers. Instead, you can define functions that perform certain actions before or after the main function logic. In the example above, the `lock` and `unlock` functions are called before and after the decrement function, respectively, but they must be called explicitly. + +As often as practicable, developers should inline functions by including the body of the function within their code using the `#[inline]` attribute. In the context of coding for Casper blockchain purposes, this reduces the overhead of executed Wasm and prevents unexpected errors due to exceeding resource tolerances. + +### Visibility + +Functions and state variables have to declare whether they are accessible by other contracts. + +Functions can be declared as: + + + +``` +`pub` inside `#[odra::module]` impl block - any contract/submodule and account can call. +`pub` inside a regular impl block - any submodule can call. +`default/no modifier/private` - only inside the contract that defines the function. +``` + + + + +``` +`public` - any contract and account can call. +`private` - only inside the contract that defines the function. +`internal` - only inside contract that inherits an internal function. +`external` - only other contracts and accounts can call + +State variables can be declared as public, private, or internal but not external. +``` + + + +### Payable + + + + +```rust showLineNumbers +use odra::{casper_types::U512, prelude::*, Address, ExecutionError, Var}; + +#[odra::module] +pub struct Payable { + owner: Var
, +} + +#[odra::module] +impl Payable { + pub fn init(&mut self) { + self.owner.set(self.env().caller()); + } + + #[odra(payable)] + pub fn deposit(&self) { + } + + pub fn not_payable(&self) { + } + + pub fn withdraw(&self) { + let amount = self.env().self_balance(); + self.env().transfer_tokens(&self.owner.get_or_revert_with(ExecutionError::UnwrapError), &amount); + } + + pub fn transfer(&self, to: Address, amount: U512) { + self.env().transfer_tokens(&to, &amount); + } +} +``` + + + + +```sol showLineNumbers +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Payable { + // Payable address can send Ether via transfer or send + address payable public owner; + + // Payable constructor can receive Ether + constructor() payable { + owner = payable(msg.sender); + } + + // Function to deposit Ether into this contract. + // Call this function along with some Ether. + // The balance of this contract will be automatically updated. + function deposit() public payable {} + + // Call this function along with some Ether. + // The function will throw an error since this function is not payable. + function notPayable() public {} + + // Function to withdraw all Ether from this contract. + function withdraw() public { + // get the amount of Ether stored in this contract + uint256 amount = address(this).balance; + + // send all Ether to owner + (bool success,) = owner.call{value: amount}(""); + require(success, "Failed to send Ether"); + } + + // Function to transfer Ether from this contract to address from input + function transfer(address payable _to, uint256 _amount) public { + // Note that "to" is declared as payable + (bool success,) = _to.call{value: _amount}(""); + require(success, "Failed to send Ether"); + } +} +``` + + + +In Odra, you can define a function with the `#[odra(payable)]` attribute to indicate that the function can receive CSPRs. In Solidity, the payable keyword is used to define functions that can receive Ether. + +### Selectors + +In Solidity, when a function is called, the first 4 bytes of calldata specify which function to call. This is called a function selector. + +```sol showLineNumbers +contract_addr.call( + abi.encodeWithSignature("transfer(address,uint256)", address, 1234) +) +``` + +Odra does not support such a mechanism. You must have access to the contract interface to call a function. + +## Events and Logging + + + + +```rust showLineNumbers +use odra::{prelude::*, Address}; + +#[odra::event] +pub struct Log { + sender: Address, + message: String, +} + +#[odra::event] +pub struct AnotherLog {} + +#[odra::module] +struct Event; + +#[odra::module] +impl Event { + pub fn test(&self) { + let env = self.env(); + env.emit_event(Log { + sender: env.caller(), + message: "Hello World!".to_string(), + }); + env.emit_event(Log { + sender: env.caller(), + message: "Hello Casper!".to_string(), + }); + env.emit_event(AnotherLog {}); + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/events/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Event { + // Event declaration + // Up to 3 parameters can be indexed. + // Indexed parameters helps you filter the logs by the indexed parameter + event Log(address indexed sender, string message); + event AnotherLog(); + + function test() public { + emit Log(msg.sender, "Hello World!"); + emit Log(msg.sender, "Hello EVM!"); + emit AnotherLog(); + } +} +``` + + + +In Odra, events are regular structs defined using the `#[odra::event]` attribute. The event struct can contain multiple fields, which can be of any type (primitive or custom Odra type). To emit an event, use the env's `emit_event()` function, passing the event struct as an argument. + +:::note +Events in Solidity are used to emit logs that off-chain services can capture. However, Casper does not support events natively. Odra mimics this feature. Read more about it in the [Basics](../basics/09-events.md) section. +::: + +## Error Handling + + + + +```rust showLineNumbers +use odra::{prelude::*, casper_types::{U256, U512}}; + +#[odra::odra_error] +pub enum CustomError { + InsufficientBalance = 1, + InputLowerThanTen = 2, +} + +#[odra::module] +pub struct Error; + +#[odra::module] +impl Error { + pub fn test_require(&mut self, i: U256) { + if i <= 10.into() { + self.env().revert(CustomError::InputLowerThanTen); + } + } + + pub fn execute_external_call(&self, withdraw_amount: U512) { + let balance = self.env().self_balance(); + if balance < withdraw_amount { + self.env().revert(CustomError::InsufficientBalance); + } + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/error/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Error { + function testRequire(uint256 _i) public pure { + // Require should be used to validate conditions such as: + // - inputs + // - conditions before execution + // - return values from calls to other functions + require(_i > 10, "Input must be greater than 10"); + } + + function testRevert(uint256 _i) public pure { + // Revert is useful when the condition to check is complex. + // This code does the exact same thing as the example above + if (_i <= 10) { + revert("Input must be greater than 10"); + } + } + + uint256 public num; + + function testAssert() public view { + // Assert should only be used to test for internal errors, + // and to check invariants. + + // Here we assert that num is always equal to 0 + // since it is impossible to update the value of num + assert(num == 0); + } + + // custom error + error InsufficientBalance(uint256 balance, uint256 withdrawAmount); + + function testCustomError(uint256 _withdrawAmount) public view { + uint256 bal = address(this).balance; + if (bal < _withdrawAmount) { + revert InsufficientBalance({ + balance: bal, + withdrawAmount: _withdrawAmount + }); + } + } +} +``` + + + +In Solidity, there are four ways to handle errors: `require`, `revert`, `assert`, and custom errors. In Odra, there is only one way to revert the execution of a function - by using the `env().revert()` function. The function takes an error type as an argument and stops the execution of the function. You define an error type using the `#[odra::odra_error]` attribute. On Casper, an error is only a number, so you can't pass a message with the error. + +## Composition vs. Inheritance +In Solidity, developers often use inheritance to reuse code and establish relationships between contracts. However, Odra and Rust follow a different paradigm known as composition. Instead of inheriting behavior from parent contracts, Odra encourages the composition of contracts by embedding one contract within another. + +Let's take a look at the difference between inheritance in Solidity and composition in Odra. + + + + +```rust showLineNumbers +use odra::{prelude::*, SubModule}; + +#[odra::module] +pub struct A; + +#[odra::module] +impl A { + pub fn foo(&self) -> String { + "A".to_string() + } +} + +#[odra::module] +pub struct B { + a: SubModule +} + +#[odra::module] +impl B { + pub fn foo(&self) -> String { + "B".to_string() + } +} + +#[odra::module] +pub struct C { + a: SubModule +} + +#[odra::module] +impl C { + pub fn foo(&self) -> String { + "C".to_string() + } +} + +#[odra::module] +pub struct D { + b: SubModule, + c: SubModule +} + +#[odra::module] +impl D { + pub fn foo(&self) -> String { + self.c.foo() + } +} + +#[odra::module] +pub struct E { + b: SubModule, + c: SubModule +} + +#[odra::module] +impl E { + pub fn foo(&self) -> String { + self.b.foo() + } +} + +#[odra::module] +pub struct F { + a: SubModule, + b: SubModule, +} + +#[odra::module] +impl F { + pub fn foo(&self) -> String { + self.a.foo() + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/inheritance/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +/* Graph of inheritance + A + / \ + B C + / \ / +F D,E +*/ + +contract A { + function foo() public pure virtual returns (string memory) { + return "A"; + } +} + +// Contracts inherit other contracts by using the keyword 'is'. +contract B is A { + // Override A.foo() + function foo() public pure virtual override returns (string memory) { + return "B"; + } +} + +contract C is A { + // Override A.foo() + function foo() public pure virtual override returns (string memory) { + return "C"; + } +} + +// Contracts can inherit from multiple parent contracts. +// When a function is called that is defined multiple times in +// different contracts, parent contracts are searched from +// right to left, and in depth-first manner. +contract D is B, C { + // D.foo() returns "C" + // since C is the right most parent contract with function foo() + function foo() public pure override(B, C) returns (string memory) { + return super.foo(); + } +} + +contract E is C, B { + // E.foo() returns "B" + // since B is the right most parent contract with function foo() + function foo() public pure override(C, B) returns (string memory) { + return super.foo(); + } +} + +// Inheritance must be ordered from β€œmost base-like” to β€œmost derived”. +// Swapping the order of A and B will throw a compilation error. +contract F is A, B { + function foo() public pure override(A, B) returns (string memory) { + return super.foo(); + } +} +``` + + + +Solidity supports both single and multiple inheritance. This means a contract can inherit from one or more contracts. Solidity uses a technique called "C3 linearization" to resolve the order in which base contracts are inherited in the case of multiple inheritance. This helps to ensure a consistent method resolution order. However, multiple inheritance can lead to complex code and potential issues, especially for inexperienced developers. + +In contrast, Rust does not have a direct equivalent to the inheritance model, but it achieves similar goals through composition. Each contract is defined as a struct, and contracts can be composed by embedding one struct within another. This approach provides a more flexible and modular way to reuse code and establish relationships between contracts. + +## Libraries and Utility + + + + +```rust showLineNumbers +use odra::{casper_types::U256, prelude::*, UnwrapOrRevert, Var}; + +mod math { + use odra::casper_types::U256; + + pub fn sqrt(y: U256) -> U256 { + let mut z = y; + if y > 3.into() { + let mut x = y / 2 + 1; + while x < z { + z = x; + x = (y / x + x) / 2; + } + } else if y != U256::zero() { + z = U256::one(); + } + z + } +} + +#[odra::module] +struct TestMath; + +#[odra::module] +impl TestMath { + pub fn test_square_root(&self, x: U256) -> U256 { + math::sqrt(x) + } +} + +#[odra::odra_error] +enum Error { + EmptyArray = 100, +} + +trait Removable { + fn remove(&mut self, index: usize); +} + +impl Removable for Var> { + fn remove(&mut self, index: usize) { + let env = self.env(); + let mut vec = self.get_or_default(); + if vec.is_empty() { + env.revert(Error::EmptyArray); + } + vec[index] = vec.pop().unwrap_or_revert(&env); + self.set(vec); + } +} + +#[odra::module] +struct TestArray { + arr: Var>, +} + +#[odra::module] +impl TestArray { + pub fn test_array_remove(&mut self) { + let mut arr = self.arr.get_or_default(); + for i in 0..3 { + arr.push(i.into()); + } + self.arr.set(arr); + + self.arr.remove(1); + + let arr = self.arr.get_or_default(); + assert_eq!(arr.len(), 2); + assert_eq!(arr[0], 0.into()); + assert_eq!(arr[1], 2.into()); + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/library/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +library Math { + function sqrt(uint256 y) internal pure returns (uint256 z) { + if (y > 3) { + z = y; + uint256 x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + // else z = 0 (default value) + } +} + +contract TestMath { + function testSquareRoot(uint256 x) public pure returns (uint256) { + return Math.sqrt(x); + } +} + +library Array { + function remove(uint256[] storage arr, uint256 index) public { + require(arr.length > 0, "Can't remove from empty array"); + arr[index] = arr[arr.length - 1]; + arr.pop(); + } +} + +contract TestArray { + using Array for uint256[]; + + uint256[] public arr; + + function testArrayRemove() public { + for (uint256 i = 0; i < 3; i++) { + arr.push(i); + } + + arr.remove(1); + + assert(arr.length == 2); + assert(arr[0] == 0); + assert(arr[1] == 2); + } +} +``` + + + +In Solidity, libraries are similar to contracts but can't declare any state variables and can't receive Ether. In the sample code above, the `Math` library contains a square root function, while the Array library provides a function to remove an element from an array. Both libraries are consumed in different ways: the `TestMath` contract calls the `sqrt` function directly, while the `TestArray` contract uses the using keyword, which extends the type `uint256[]` by adding the `remove` function. + +In Odra, you use language-level features: modules and traits. The mod keyword defines a module, which is similar to a library in Solidity. Modules can contain functions, types, and other items that can be reused across multiple contracts. Traits are similar to interfaces in other programming languages, defining a set of functions that a type must implement. Implementing the `Removable` trait for the `Var>` type allows the `remove` function to be called on a variable that stores a vector of `U256` values. + +## Fallback and Receive Functions + +In Solidity, a contract receiving Ether must implement a `receive()` and/or `fallback()` function. The `receive()` function is called when Ether is sent to the contract with no data, while the `fallback()` function is called when the contract receives Ether with data or when a function that does not exist is called. + +Odra does not have a direct equivalent to the `receive()` and `fallback()` functions. Instead, you can define a function with the `#[odra(payable)]` attribute to indicate that the function can receive CSPRs. + +## Miscellaneous + + +### Hashing + + + +```rust showLineNumbers +use odra::{ + casper_types::{bytesrepr::ToBytes, U256}, + prelude::*, + Address, UnwrapOrRevert, Var, +}; + +#[odra::module] +pub struct HashFunction; + +#[odra::module] +impl HashFunction { + pub fn hash(&self, text: String, num: U256, addr: Address) -> [u8; 32] { + let env = self.env(); + let mut data = Vec::new(); + data.extend(text.to_bytes().unwrap_or_revert(&env)); + data.extend(num.to_bytes().unwrap_or_revert(&env)); + data.extend(addr.to_bytes().unwrap_or_revert(&env)); + env.hash(data) + } +} + +#[odra::module] +pub struct GuessTheMagicWord { + answer: Var<[u8; 32]>, +} + +#[odra::module] +impl GuessTheMagicWord { + /// Initializes the contract with the magic word hash. + pub fn init(&mut self) { + self.answer.set([ + 0x86, 0x67, 0x15, 0xbb, 0x0b, 0x96, 0xf1, 0x06, 0xe0, 0x68, 0x07, 0x89, 0x22, 0x84, + 0x42, 0x81, 0x19, 0x6b, 0x1e, 0x61, 0x45, 0x50, 0xa5, 0x70, 0x4a, 0xb0, 0xa7, 0x55, + 0xbe, 0xd7, 0x56, 0x08, + ]); + } + + /// Checks if the `word` is the magic word. + pub fn guess(&self, word: String) -> bool { + let env = self.env(); + let hash = env.hash(word.to_bytes().unwrap_or_revert(&env)); + hash == self.answer.get_or_default() + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/hashing/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract HashFunction { + function hash(string memory _text, uint256 _num, address _addr) + public + pure + returns (bytes32) + { + return keccak256(abi.encodePacked(_text, _num, _addr)); + } +} + +contract GuessTheMagicWord { + bytes32 public answer = + 0x60298f78cc0b47170ba79c10aa3851d7648bd96f2f8e46a19dbc777c36fb0c00; + + // Magic word is "Solidity" + function guess(string memory _word) public view returns (bool) { + return keccak256(abi.encodePacked(_word)) == answer; + } +} +``` + + + +The key difference between the two is that in Solidity, the `keccak256` function is used to hash data, while in Odra, the `env.hash()` function is used, which implements the `blake2b` algorithm. Both functions take a byte array as input and return a 32-byte hash. + +### Try-catch + + + + +```rust showLineNumbers +use odra::{module::Module, Address, ContractRef, External, Var}; + +#[odra::module] +pub struct Example { + other_contract: External, +} + +#[odra::module] +impl Example { + pub fn init(&mut self, other_contract: Address) { + self.other_contract.set(other_contract); + } + + pub fn execute_external_call(&self) { + let result = self.other_contract.some_function(); + match result { + Ok(success) => { + // Code to execute if the external call was successful + } + Err(reason) => { + // Code to execute if the external call failed + } + } + } +} + +#[odra::module] +pub struct OtherContract; + +#[odra::module] +impl OtherContract { + pub fn some_function(&self) -> Result { + Ok(true) + } +} +``` + + + + +```sol showLineNumbers title="https://solidity-by-example.org/hashing/" +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +contract Example { + OtherContract otherContract; + + constructor(address _otherContractAddress) public { + otherContract = OtherContract(_otherContractAddress); + } + + function executeExternalCall() public { + try otherContract.someFunction() returns (bool success) { + // Code to execute if the external call was successful + require(success, "Call failed"); + } catch Error(string memory reason) { + // Code to execute if the external call failed with a revert reason + // Optionally handle specific revert reasons + emit LogErrorString(reason); + } catch (bytes memory lowLevelData) { + // Code to execute if the external call failed without a revert reason + emit LogErrorBytes(lowLevelData); + } + } + + event LogErrorString(string reason); + event LogErrorBytes(bytes lowLevelData); +} + +contract OtherContract { + function someFunction() public returns (bool) { + // Function logic + } +} +``` + + + +In Solidity, `try/catch` is a feature that allows developers to handle exceptions and errors more gracefully. The `try/catch` statement allows developers to catch and handle exceptions that occur during external function calls and contract creation. + +In Odra, there is no direct equivalent to the `try/catch` statement in Solidity. However, you can use the `Result` type to handle errors in a similar way. The `Result` type is an enum that represents either success (`Ok`) or failure (`Err`). You can use the match statement to handle the Result type and execute different code based on the result. However, if an unexpected error occurs on the way, the whole transaction reverts. + +## Conclusion +Congratulations! You've now learned the main differences in writing smart contracts with the Odra Framework. By understanding the structure, initialization, error handling, and the composition pattern in Odra, you can effectively transition from Solidity to Odra for Casper blockchain development. + +Experiment with the provided code samples, explore more advanced features, and unleash the full potential of the Odra Framework. + +Read more about the Odra Framework in the [Basics](../category/basics) and [Advanced](../category/advanced/) sections. + +Learn by example with our [Tutorial](../category/tutorials) series, you will find there a contract you likely familiar with - the [Erc20](../tutorials/erc20.md) standard implementation. + +If you have any further questions or need clarification on specific topics, feel free to join our [Discord](https://discord.com/invite/Mm5ABc9P8k)! diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/ownable.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/ownable.md new file mode 100644 index 000000000..9396a4fc6 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/ownable.md @@ -0,0 +1,207 @@ +--- +sidebar_position: 1 +--- + +# Ownable +In this tutorial, we will write a simple module that allows us to set its owner. Later, it can be reused to limit access to the contract's critical features. + +## Framework features +A module we will write in a minute, will help you master a few Odra features: + +* storing a single value, +* defining a constructor, +* error handling, +* defining and emitting `events`. +* registering a contact in a test environment, +* interactions with the test environment, +* assertions (value, events, errors assertions). + +## Code + +Before we write any code, we define functionalities we would like to implement. + +1. Module has an initializer that should be called once. +2. Only the current owner can set a new owner. +3. Read the current owner. +4. A function that fails if called by a non-owner account. + +### Define a module + +```rust title=ownable.rs showLineNumbers +use odra::prelude::*; +use odra::{Address, Var}; + +#[odra::module(events = [OwnershipChanged])] +pub struct Ownable { + owner: Var> +} +``` +That was easy, but it is crucial to understand the basics before we move on. + +* **L4** - Firstly, we need to create a struct called `Ownable` and apply `#[odra::module(events = [OwnershipChanged])]` attribute to it. The `events` attribute is optional but informs the Odra toolchain about the events that will be emitted by the module and includes them in the contract's metadata. `OwnershipChanged` is a type that will be defined later. +* **L6** - Then we can define the layout of our module. It is extremely simple - just a single state value. What is most important is that you can never leave a raw type; you must always wrap it with `Var`. + +### Init the module + +```rust title=ownable.rs showLineNumbers +#[odra::module] +impl Ownable { + pub fn init(&mut self, owner: Address) { + if self.owner.get_or_default().is_some() { + self.env().revert(Error::OwnerIsAlreadyInitialized) + } + + self.owner.set(Some(owner)); + + self.env().emit_event(OwnershipChanged { + prev_owner: None, + new_owner: owner + }); + } +} + +#[odra::odra_error] +pub enum Error { + OwnerIsAlreadyInitialized = 1, +} + +#[odra::event] +pub struct OwnershipChanged { + pub prev_owner: Option
, + pub new_owner: Address +} +``` + +Ok, we have done a couple of things, let's analyze them one by one: +* **L1** - The `impl` should be an Odra module, so add `#[odra::module]`. +* **L3** - The `init` function is a constructor. This matters if we would like to deploy the `Ownable` module as a standalone contract. +* **L17-L20** - Before we set a new owner, we must assert there was no owner before and raise an error otherwise. For that purpose, we defined an `Error` enum. Notice that the `#[odra::odra_error]` attribute is applied to the enum. It generates, among others, the required `Into` binding. +* **L4-L6** - If the owner has been set already, we call `ContractEnv::revert()` function with an `Error::OwnerIsAlreadyInitialized` argument. +* **L8** - Then we write the owner passed as an argument to the storage. To do so, we call the `set()` on `Var`. +* **L22-L26** - Once the owner is set, we would like to inform the outside world. The first step is to define an event struct. The struct annotated with `#[odra::event]` attribute. +* **L10** - Finally, call `ContractEnv::emit_event()` passing the `OwnershipChanged` instance to the function. Hence, we set the first owner, we set the `prev_owner` value to `None`. +### Features implementation + +``` rust title=ownable.rs showLineNumbers +#[odra::module] +impl Ownable { + ... + + pub fn ensure_ownership(&self, address: &Address) { + if Some(address) != self.owner.get_or_default().as_ref() { + self.env().revert(Error::NotOwner) + } + } + + pub fn change_ownership(&mut self, new_owner: &Address) { + self.ensure_ownership(&self.env().caller()); + let current_owner = self.get_owner(); + self.owner.set(Some(*new_owner)); + self.env().emit_event(OwnershipChanged { + prev_owner: Some(current_owner), + new_owner: *new_owner + }); + } + + pub fn get_owner(&self) -> Address { + match self.owner.get_or_default() { + Some(owner) => owner, + None => self.env().revert(Error::OwnerIsNotInitialized) + } + } +} + +#[odra::odra_error] +pub enum Error { + NotOwner = 1, + OwnerIsAlreadyInitialized = 2, + OwnerIsNotInitialized = 3, +} +``` +The above implementation relies on the concepts we have already used in this tutorial, so it should be easy for you to get along. + +* **L7,L31** - `ensure_ownership()` reads the current owner and reverts if it does not match the input `Address`. Also, we need to update our `Error` enum by adding a new variant `NotOwner`. +* **L11** - The function defined above can be reused in the `change_ownership()` implementation. We pass to it the current caller, using the `ContractEnv::caller()` function. Then we update the state and emit `OwnershipChanged`. +* **L21,L33** - Lastly, a getter function. Read the owner from storage, if the getter is called on an uninitialized module, it should revert with a new `Error` variant `OwnerIsNotInitialized`. There is one worth-mentioning subtlety: `Var::get()` function returns `Option`. If the type implements the `Default` trait, you can call the `get_or_default()` function, and the contract does not fail even if the value is not initialized. As the `owner` is of type `Option
` the `Var::get()` would return `Option>`, we use `Var::get_or_default()` instead. + +### Test + +```rust title=ownable.rs showLineNumbers +#[cfg(test)] +mod tests { + use super::*; + use odra::host::{Deployer, HostEnv, HostRef}; + + fn setup() -> (OwnableHostRef, HostEnv, Address) { + let env: HostEnv = odra_test::env(); + let init_args = OwnableInitArgs { + owner: env.get_account(0) + }; + (OwnableHostRef::deploy(&env, init_args), env.clone(), env.get_account(0)) + } + + #[test] + fn initialization_works() { + let (ownable, env, owner) = setup(); + assert_eq!(ownable.get_owner(), owner); + + env.emitted_event( + &ownable, + &OwnershipChanged { + prev_owner: None, + new_owner: owner + } + ); + } + + #[test] + fn owner_can_change_ownership() { + let (mut ownable, env, owner) = setup(); + let new_owner = env.get_account(1); + + env.set_caller(owner); + ownable.change_ownership(&new_owner); + assert_eq!(ownable.get_owner(), new_owner); + + env.emitted_event( + &ownable, + &OwnershipChanged { + prev_owner: Some(owner), + new_owner + } + ); + } + + #[test] + fn non_owner_cannot_change_ownership() { + let (mut ownable, env, _) = setup(); + let new_owner = env.get_account(1); + ownable.change_ownership(&new_owner); + + assert_eq!( + ownable.try_change_ownership(&new_owner), + Err(Error::NotOwner.into()) + ); + } +} +``` +* **L6** - Each test case starts with the same initialization process, so for convenience, we have defined the `setup()` function, which we call in the first statement of each test. Take a look at the signature: `fn setup() -> (OwnableHostRef, HostEnv, Address)`. `OwnableHostRef` is a contract reference generated by Odra. This reference allows us to call all the defined entrypoints, namely: `ensure_ownership()`, `change_ownership()`, `get_owner()`, but not `init()`, which is a constructor. +* **L7-L11** - The starting point of every test is getting an instance of `HostEnv` by calling `odra_test::env()`. Our function returns a triple: a contract ref, an env, and an address (the initial owner). Odra's `#[odra::module]` attribute implements a `odra::host::Deployer` for `OwnableHostRef`, and `OwnableInitArgs` that we pass as the second argument of the `odra::host::Deployer::deploy()` function. Lastly, the module needs an owner. The easiest way is to take one from the `HostEnv`. We choose the address of first account (which is the default one). +* **L14** - It is time to define the first test. As you see, it is a regular Rust test. +* **L16-17** - Using the `setup()` function, we get the owner and a reference (in this test, we don't use the env, so we ignore it). We make a standard assertion, comparing the owner we know with the value returned from the contract. +:::note +You may have noticed, we use here the term `module` interchangeably with `contract`. The reason is once we deploy our module onto a virtual blockchain it may be considered a contract. +::: +* **L19-25** - On the contract, only the `init()` function has been called, so we expect one event to have been emitted. To assert that, let's use `HostEnv`. To get the env, we call `env()` on the contract, then call `HostEnv::emitted_event`. As the first argument, pass the contract you want to read events from, followed by an event as you expect it to have occurred. +* **L31** - Because we know the initial owner is the 0th account, we must select a different account. It could be any index from 1 to 19 - the `HostEnv` predefines 20 accounts. +* **L33** - As mentioned, the default is the 0th account, if you want to change the executor, call the `HostEnv::set_caller()` function. +:::note +The caller switch applies only the next contract interaction, the second call will be done as the default account. +::: +* **L46-55** - If a non-owner account tries to change ownership, we expect it to fail. To capture the error, call `HostEnv::try_change_ownership()` instead of `HostEnv::change_ownership()`. `HostEnv` provides try_ functions for each contract's entrypoint. The `try` functions return `OdraResult` (an alias for `Result`) instead of panicking and halting the execution. In our case, we expect the contract to revert with the `Error::NotOwner` error. To compare the error, we use the `Error::into()` function, which converts the error into the `OdraError` type. + +## Summary +The `Ownable` module is ready, and we can test it against any defined backend. Theoretically it can be deployed as a standalone contract, but in upcoming tutorials you will see how to use it to compose a more complex contract. + +## What's next +In the next tutorial we will implement a ERC20 standard. \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/owned-token.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/owned-token.md new file mode 100644 index 000000000..3f2b32791 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/owned-token.md @@ -0,0 +1,110 @@ +--- +sidebar_position: 3 +--- + +# OwnedToken + +This tutorial shows the great power of the modularization-focused design of the Odra Framework. We are going to use the modules we built in the last two tutorials to build a new one. + +## Code +What should our module be capable of? + +1. Conform the Erc20 interface. +2. Allow only the module owner to mint tokens. +3. Enable the current owner to designate a new owner. + + +### Module definition + +Let's define a module called `OwnedToken` that is a composition of `Ownable` and `Erc20` modules. + +```rust title=owned_token.rs showLineNumbers +use crate::{erc20::Erc20, ownable::Ownable}; +use odra::prelude::*; +use odra::module::SubModule; + +#[odra::module] +pub struct OwnedToken { + ownable: SubModule, + erc20: SubModule +} +``` + +As you can see, we do not need any storage definition - we just take advantage of the already-defined modules! + +### Delegation + +```rust title=owned_token.rs showLineNumbers +... +use odra::{Address, casper_types::U256}; +... + +#[odra::module] +impl OwnedToken { + pub fn init(&mut self, name: String, symbol: String, decimals: u8, initial_supply: U256) { + let deployer = self.env().caller(); + self.ownable.init(deployer); + self.erc20.init(name, symbol, decimals, initial_supply); + } + + pub fn name(&self) -> String { + self.erc20.name() + } + + pub fn symbol(&self) -> String { + self.erc20.symbol() + } + + pub fn decimals(&self) -> u8 { + self.erc20.decimals() + } + + pub fn total_supply(&self) -> U256 { + self.erc20.total_supply() + } + + pub fn balance_of(&self, address: &Address) -> U256 { + self.erc20.balance_of(address) + } + + pub fn allowance(&self, owner: &Address, spender: &Address) -> U256 { + self.erc20.allowance(owner, spender) + } + + pub fn transfer(&mut self, recipient: &Address, amount: &U256) { + self.erc20.transfer(recipient, amount); + } + + pub fn transfer_from(&mut self, owner: &Address, recipient: &Address, amount: &U256) { + self.erc20.transfer_from(owner, recipient, amount); + } + + pub fn approve(&mut self, spender: &Address, amount: &U256) { + self.erc20.approve(spender, amount); + } + + pub fn get_owner(&self) -> Address { + self.ownable.get_owner() + } + + pub fn change_ownership(&mut self, new_owner: &Address) { + self.ownable.change_ownership(new_owner); + } + + pub fn mint(&mut self, address: &Address, amount: &U256) { + self.ownable.ensure_ownership(&self.env().caller()); + self.erc20.mint(address, amount); + } +} +``` + +Easy. However, there are a few worth mentioning subtleness: + +* **L9-L10** - A constructor is an excellent place to initialize both modules at once. +* **L13-L15** - Most of the entrypoints do not need any modification, so we simply delegate them to the `erc20` module. +* **L49-L51** - The same is done with the `ownable` module. +* **L57-L60** - Minting should not be unconditional, we need some control over it. First, using `ownable` we make sure the `caller` really is indeed the owner. + +## Summary + +The Odra Framework encourages a modularized design of your smart contracts. You can encapsulate features in smaller units and test them in isolation, ensuring your project is easy to maintain. Finally, unleash their full potential by combining modules. You do not need any magic bindings for that. \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/pauseable.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/pauseable.md new file mode 100644 index 000000000..b80ae1e23 --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/pauseable.md @@ -0,0 +1,173 @@ +--- +sidebar_position: 5 +--- + +# Pausable + +The `Pausable` module is like your smart contract's safety switch. It lets authorized users temporarily pause certain features if needed. It's a great way to boost security, but it's not meant to be used on its own. Think of it as an extra tool in your access control toolbox, giving you more control to manage your smart contract safely and efficiently. + +## Code + +As always, we will start with defining functionalities of our module. + +1. Check the state - is it paused or not. +2. State guards - a contract should stop execution if is in a state we don't expect. +3. Switch the state. + +### Events and Error + +There just two errors that may occur: `PausedRequired`, `UnpausedRequired`. We define them in a standard Odra way. + +Events definition is highly uncomplicated: `Paused` and `Unpaused` events holds only the address of the pauser. + +```rust title=pauseable.rs showLineNumbers +use odra::prelude::*; +use odra::Address; + +#[odra::odra_error] +pub enum Error { + PausedRequired = 1_000, + UnpausedRequired = 1_001, +} + +#[odra::event] +pub struct Paused { + pub account: Address +} + +#[odra::event] +pub struct Unpaused { + pub account: Address +} +``` + +### Module definition + +The module storage is extremely simple - has a single `Var` of type bool, that indicates if a contract is paused. + +```rust title=pauseable.rs showLineNumbers +use odra::Var; +... + +#[odra::module(events = [Paused, Unpaused])] +pub struct Pausable { + is_paused: Var +} +``` + +### Checks and guards + +Now, let's move to state checks and guards. + +```rust title=pauseable.rs showLineNumbers +impl Pausable { + pub fn is_paused(&self) -> bool { + self.is_paused.get_or_default() + } + + pub fn require_not_paused(&self) { + if self.is_paused() { + self.env().revert(Error::UnpausedRequired); + } + } + + pub fn require_paused(&self) { + if !self.is_paused() { + self.env().revert(Error::PausedRequired); + } + } +} +``` +* **L1** - as mentioned in the intro, the module is not intended to be a standalone contract, so the only `impl` block is not annotated with `odra::module` and hence does not expose any entrypoint. +* **L2** - `is_paused()` checks the contract state, if the Var `is_paused` has not been initialized, the default value (false) is returned. +* **L6** - to guarantee the code is executed when the contract is not paused, `require_not_paused()` function reads the state and reverts if the contract is paused. +* **L12** - `require_paused()` is a mirror function - stops the contract execution if the contract is not paused. + +### Actions + +Finally, we will add the ability to switch the module state. + +```rust title=pauseable.rs showLineNumbers +impl Pausable { + pub fn pause(&mut self) { + self.require_not_paused(); + self.is_paused.set(true); + + self.env().emit_event(Paused { + account: self.env().caller() + }); + } + + pub fn unpause(&mut self) { + self.require_paused(); + self.is_paused.set(false); + + self.env().emit_event(Unpaused { + account: self.env().caller() + }); + } +} +``` + +`pause()` and `unpause()` functions do three things: ensure the contract is the right state (unpaused for `pause()`, not paused for `unpause()`), updates the state, and finally emits events (`Paused`/`Unpaused`). + + +## Pausable counter + +In the end, let's use the module in a contract. For this purpose, we will implement a mock contract called `PausableCounter`. The contract consists of a Var `value` and a `Pausable` module. The counter can only be incremented if the contract is in a normal state (is not paused). + +```rust title=pauseable.rs showLineNumbers +... +use odra::SubModule; +... + +#[odra::module] +pub struct PausableCounter { + value: Var, + pauseable: SubModule +} + +#[odra::module] +impl PausableCounter { + pub fn increment(&mut self) { + self.pauseable.require_not_paused(); + + let new_value = self.value.get_or_default() + 1; + self.value.set(new_value); + } + + pub fn pause(&mut self) { + self.pauseable.pause(); + } + + pub fn unpause(&mut self) { + self.pauseable.unpause(); + } + + pub fn get_value(&self) -> u32 { + self.value.get_or_default() + } +} + +#[cfg(test)] +mod test { + use super::*; + use odra::host::{Deployer, NoArgs}; + + #[test] + fn increment_only_if_unpaused() { + let test_env = odra_test::env(); + let mut contract = PausableCounterHostRef::deploy(&test_env, NoArgs); + contract.increment(); + contract.pause(); + + assert_eq!( + contract.try_increment().unwrap_err(), + Error::UnpausedRequired.into() + ); + assert_eq!(contract.get_value(), 1); + } +} +``` + +As we see in the test, in a simple way, using a single function call we can turn off the counter for a while and freeze the counter. Any time we want we can turn it back on. Easy! \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/tutorials/using-proxy-caller.md b/docusaurus/versioned_docs/version-1.2.0/tutorials/using-proxy-caller.md new file mode 100644 index 000000000..04bb2eaca --- /dev/null +++ b/docusaurus/versioned_docs/version-1.2.0/tutorials/using-proxy-caller.md @@ -0,0 +1,332 @@ +--- +sidebar_position: 8 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Using Proxy Caller + +In this tutorial, we will learn how to use the `proxy_caller` wasm to call an Odra [payable](../backends/03-casper.md#payable) function. The `proxy_caller` is a session code that top-ups the `cargo_purse` passes it as an argument and then calls the contract. This is useful when you want to call a payable function attaching some `CSPR`s to the call. + +Read more about the `proxy_caller` [here](../backends/03-casper.md#using-proxy_callerwasm). + +## Contract +For this tutorial, we will use the `TimeLockWallet` contract from our examples. + +```rust title=examples/src/contracts/tlw.rs showLineNumbers +use odra::prelude::*; +use odra::{casper_types::U512, Address, Mapping, Var}; + +#[odra::module(errors = Error, events = [Deposit, Withdrawal])] +pub struct TimeLockWallet { + balances: Mapping, + lock_expiration_map: Mapping, + lock_duration: Var +} + +#[odra::module] +impl TimeLockWallet { + /// Initializes the contract with the lock duration. + pub fn init(&mut self, lock_duration: u64) { + self.lock_duration.set(lock_duration); + } + + /// Deposits the tokens into the contract. + #[odra(payable)] + pub fn deposit(&mut self) { + // Extract values + let caller: Address = self.env().caller(); + let amount: U512 = self.env().attached_value(); + let current_block_time: u64 = self.env().get_block_time(); + + // Multiple lock check + if self.balances.get(&caller).is_some() { + self.env().revert(Error::CannotLockTwice) + } + + // Update state, emit event + self.balances.set(&caller, amount); + self.lock_expiration_map + .set(&caller, current_block_time + self.lock_duration()); + self.env().emit_event(Deposit { + address: caller, + amount + }); + } + + /// Withdraws the tokens from the contract. + pub fn withdraw(&mut self, amount: &U512) { + // code omitted for brevity + } + + /// Returns the balance of the given account. + pub fn get_balance(&self, address: &Address) -> U512 { + // code omitted for brevity + } + + /// Returns the lock duration. + pub fn lock_duration(&self) -> u64 { + // code omitted for brevity + } +} + +/// Errors that may occur during the contract execution. +#[odra::odra_error] +pub enum Error { + LockIsNotOver = 1, + CannotLockTwice = 2, + InsufficientBalance = 3 +} + +/// Deposit event. +#[odra::event] +pub struct Deposit { + pub address: Address, + pub amount: U512 +} + +/// Withdrawal event. +#[odra::event] +pub struct Withdrawal { + pub address: Address, + pub amount: U512 +} +``` + +Full code can be found [here](https://github.com/odradev/odra/blob/release/1.1.0/examples/src/contracts/tlw.rs). + +## Client + +Before we can interact with the node, we need to set it up. We will use the [`casper-nctl-docker`](https://github.com/make-software/casper-nctl-docker) image. + +```bash +docker run --rm -it --name mynctl -d -p 11101:11101 -p 14101:14101 -p 18101:18101 makesoftware/casper-nctl +``` + +Make sure you have the contract's wasm file and the secret key. + +```bash +# Build the contract +cargo odra build -c TimeLockWallet +# Extract secret key +docker exec mynctl /bin/bash -c "cat /home/casper/casper-node/utils/nctl/assets/net-1/users/user-1/secret_key.pem" > your/path/secret_key.pem +``` + + + + +To interact with the contract, we use the `livenet` backend. It allows to write the code in the same manner as the test code, but it interacts with the live network (a local node in our case). + + +```toml title=Cargo.toml +[package] +name = "odra-examples" +version = "1.1.0" +edition = "2021" + +[dependencies] +odra = { path = "../odra", default-features = false } +... # other dependencies +odra-casper-livenet-env = { version = "1.1.0", optional = true } + +... # other sections + +[features] +default = [] +livenet = ["odra-casper-livenet-env"] + +... # other sections + +[[bin]] +name = "tlw_on_livenet" +path = "bin/tlw_on_livenet.rs" +required-features = ["livenet"] +test = false + +... # other sections +``` + +```rust title=examples/bin/tlw_on_livenet.rs showLineNumbers +//! Deploys an [odra_examples::contracts::tlw::TimeLockWallet] contract, then deposits and withdraw some CSPRs. +use odra::casper_types::{AsymmetricType, PublicKey, U512}; +use odra::host::{Deployer, HostRef}; +use odra::Address; +use odra_examples::contracts::tlw::{TimeLockWalletHostRef, TimeLockWalletInitArgs}; + +const DEPOSIT: u64 = 100; +const WITHDRAWAL: u64 = 99; +const GAS: u64 = 20u64.pow(9); + +fn main() { + let env = odra_casper_livenet_env::env(); + let caller = env.get_account(0); + + env.set_caller(caller); + env.set_gas(GAS); + + let mut contract = TimeLockWalletHostRef::deploy( + &env, + TimeLockWalletInitArgs { lock_duration: 60 * 60 } + ); + // Send 100 CSPRs to the contract. + contract + .with_tokens(U512::from(DEPOSIT)) + .deposit(); + + println!("Caller's balance: {:?}", contract.get_balance(&caller)); + // Withdraw 99 CSPRs from the contract. + contract.withdraw(&U512::from(WITHDRAWAL)); + println!("Remaining balance: {:?}", contract.get_balance(&caller)); +} +``` + +To run the code, execute the following command: + +```bash +ODRA_CASPER_LIVENET_SECRET_KEY_PATH=.node-keys/secret_key.pem \ +ODRA_CASPER_LIVENET_NODE_ADDRESS=http://localhost:11101 \ +ODRA_CASPER_LIVENET_CHAIN_NAME=casper-net-1 \ +cargo run --bin tlw_on_livenet --features=livenet +# Sample output +πŸ’ INFO : Deploying "TimeLockWallet". +πŸ’ INFO : Found wasm under "wasm/TimeLockWallet.wasm". +πŸ™„ WAIT : Waiting 15s for "74f0df4bc65cdf9e05bca70a8b786bd0f528858f26e11f5a9866dfe286551558". +πŸ’ INFO : Deploy "74f0df4bc65cdf9e05bca70a8b786bd0f528858f26e11f5a9866dfe286551558" successfully executed. +πŸ’ INFO : Contract "hash-cce6a97e0db6feea0c4d99f670196c9462e0789fb3cdedd3dfbc6dfcbf66252e" deployed. +πŸ’ INFO : Calling "hash-cce6a97e0db6feea0c4d99f670196c9462e0789fb3cdedd3dfbc6dfcbf66252e" with entrypoint "deposit" through proxy. +πŸ™„ WAIT : Waiting 15s for "bd571ab64c13d2b2fdb8e0e6dd8473b696349dfb5a891b55dbe9f33d017057d3". +πŸ’ INFO : Deploy "bd571ab64c13d2b2fdb8e0e6dd8473b696349dfb5a891b55dbe9f33d017057d3" successfully executed. +Caller's balance: 100 +πŸ’ INFO : Calling "hash-cce6a97e0db6feea0c4d99f670196c9462e0789fb3cdedd3dfbc6dfcbf66252e" with entrypoint "withdraw". +πŸ™„ WAIT : Waiting 15s for "57f9aadbd77cbfbbe9b2ba54759d025f94203f9230121289fa37585f8b17020e". +πŸ’ INFO : Deploy "57f9aadbd77cbfbbe9b2ba54759d025f94203f9230121289fa37585f8b17020e" successfully executed. +Remaining balance: 1 +``` + +As observed, the contract was successfully deployed, and the `Caller` deposited tokens. Subsequently, the caller withdrew 99 CSPRs from the contract, leaving the contract's balance at 1 CSPR. +The logs display deploy hashes, the contract's hash, and even indicate if the call was made through the proxy, providing a comprehensive overview of the on-chain activity. + + + + + +Since TypeScript code often requires considerable boilerplate, we offer a streamlined version of the code. We demonstrate how to deploy the contract and prepare a deploy that utilizes the `proxy_caller` to invoke a payable function with attached `CSPR` tokens. The [previous tutorial](./build-deploy-read.md) details how to read the state, which is not the focus of our current discussion. + +```typescript title=index.ts showLineNumbers +import { + CLByteArray, + CLList, + CLU8, + CLValueBuilder, + CasperClient, + Contracts, + Keys, + RuntimeArgs, + csprToMotes, + decodeBase16, +} from "casper-js-sdk"; +import fs from "fs"; + +const LOCAL_NODE_URL = "http://127.0.0.1:11101/rpc"; +const SECRET_KEY_PATH = "keys/secret_key.pem" +const PROXY_CALLER_PATH = "wasm/proxy_caller.wasm" +const CONTRACT_PATH = "wasm/TimeLockWallet.wasm"; +const CHAIN_NAME = "casper-net-1"; +const ENTRY_POINT = "deposit"; +const DEPOSIT = 100; +const GAS = 110; +// Once the contract is deployed, the contract package hash +// can be obtained from the global state. +const CONTRACT_PACKAGE_HASH = "..."; + +const casperClient = new CasperClient(LOCAL_NODE_URL); +const keypair = Keys.Ed25519.loadKeyPairFromPrivateFile( + SECRET_KEY_PATH +); +const contract = new Contracts.Contract(casperClient); + +export async function deploy_contract(): Promise { + // Required odra_cfg args and the constructor args + const args = RuntimeArgs.fromMap({ + odra_cfg_package_hash_key_name: CLValueBuilder.string("tlw"), + odra_cfg_allow_key_override: CLValueBuilder.bool(true), + odra_cfg_is_upgradable: CLValueBuilder.bool(true), + lock_duration: CLValueBuilder.u64(60 * 60) + }); + + const wasm = new Uint8Array(fs.readFileSync(CONTRACT_PATH)); + const deploy = contract.install( + wasm, + args, + csprToMotes(GAS).toString(), + keypair.publicKey, + CHAIN_NAME, + [keypair], + ); + return casperClient.putDeploy(deploy); +} + +export async function deposit(): Promise { + // Contract package hash is a 32-byte array, + // so take the hex string and convert it to a byte array. + // This is done using the decodeBase16 function from + // the casper-js-sdk. + const contractPackageHashBytes = new CLByteArray( + decodeBase16(CONTRACT_PACKAGE_HASH) + ); + // Next, create RuntimeArgs for the deploy + // and pass them as bytes to the contract. + // Note that the args are not a byte array, but a CLList + // of CLU8s - a different type of CLValue. + // Finally, create a Uint8Array from the bytes and + // then transform it into a CLList. + const args_bytes: Uint8Array = RuntimeArgs.fromMap({}) + .toBytes() + .unwrap(); + const serialized_args = new CLList( + Array.from(args_bytes) + .map(value => new CLU8(value)) + ); + + const args = RuntimeArgs.fromMap({ + attached_value: CLValueBuilder.u512(DEPOSIT), + amount: CLValueBuilder.u512(DEPOSIT), + entry_point: CLValueBuilder.string(ENTRY_POINT), + contract_package_hash: contractPackageHashBytes, + args: serialized_args + }); + // Use proxy_caller to send tokens to the contract. + const wasm = new Uint8Array(fs.readFileSync(PROXY_CALLER_PATH)); + const deploy = contract.install( + wasm, + args, + csprToMotes(GAS).toString(), + keypair.publicKey, + CHAIN_NAME, + [keypair], + ); + return casperClient.putDeploy(deploy); +} + +deploy_contract() + .then((result) => { console.log(result); }); + +// One you obatin the contract hash, you can call the deposit function: +// deposit() +// .then((result) => { console.log(result); }); +``` + +To run the code, execute the following command: + +```sh +tsc && node target/index.js +# Sample output +f40e3ca983034435d829462dd53d801df4e98013009cbf4a6654b3ee467063a1 # the deploy hash +``` + + + +## Conclusion + +In this tutorial, we learned how to use the `proxy_caller` wasm to make a payable function call. We deployed the `TimeLockWallet` contract, deposited tokens using the `proxy_caller` with attached CSPRs, and withdrew them. You got to try it out in both `Rust` and `TypeScript`, so you can choose whichever you prefer. `Rust` code seemed simpler, thanks to the Odra `livenet` backend making chain interactions easier to handle. \ No newline at end of file diff --git a/docusaurus/versioned_docs/version-1.2.0/wallet.png b/docusaurus/versioned_docs/version-1.2.0/wallet.png new file mode 100644 index 000000000..7ac8d99fd Binary files /dev/null and b/docusaurus/versioned_docs/version-1.2.0/wallet.png differ diff --git a/docusaurus/versioned_sidebars/version-1.2.0-sidebars.json b/docusaurus/versioned_sidebars/version-1.2.0-sidebars.json new file mode 100644 index 000000000..caea0c03b --- /dev/null +++ b/docusaurus/versioned_sidebars/version-1.2.0-sidebars.json @@ -0,0 +1,8 @@ +{ + "tutorialSidebar": [ + { + "type": "autogenerated", + "dirName": "." + } + ] +} diff --git a/docusaurus/versions.json b/docusaurus/versions.json index cc9b18e79..4bc6768e0 100644 --- a/docusaurus/versions.json +++ b/docusaurus/versions.json @@ -1,4 +1,5 @@ [ + "1.2.0", "1.1.0", "1.0.0", "0.9.1",