From 3555eb5152ac3d56872aae4aeef82deda24d0ac4 Mon Sep 17 00:00:00 2001 From: petarjuki7 Date: Fri, 20 Dec 2024 15:07:15 +0100 Subject: [PATCH] Diferentiate rust sdk and tutorial --- .../03-protocol-sdk/02-protocol-rs-sdk.mdx | 661 ++---------------- docs/06-tutorials/04-build-app.mdx | 638 +++++++++++++++++ docs/06-tutorials/_04-build-app.mdx | 6 - 3 files changed, 715 insertions(+), 590 deletions(-) create mode 100644 docs/06-tutorials/04-build-app.mdx delete mode 100644 docs/06-tutorials/_04-build-app.mdx diff --git a/docs/05-developer-tools/02-SDK/03-protocol-sdk/02-protocol-rs-sdk.mdx b/docs/05-developer-tools/02-SDK/03-protocol-sdk/02-protocol-rs-sdk.mdx index d0032c9d..422a63ed 100644 --- a/docs/05-developer-tools/02-SDK/03-protocol-sdk/02-protocol-rs-sdk.mdx +++ b/docs/05-developer-tools/02-SDK/03-protocol-sdk/02-protocol-rs-sdk.mdx @@ -3,640 +3,133 @@ id: protocol-rs-sdk title: Rust Protocol SDK --- -## Getting Started with Calimero SDK for Rust +## Calimero SDK Macros for Rust -The Calimero SDK for Rust empowers developers to build applications that compile -to WebAssembly (Wasm) and run securely within the Calimero virtual machine (VM). -This guide will walk you through setting up a Rust project using the Calimero -SDK, writing an application, and preparing it for deployment. +This guide provides a comprehensive reference of the essential +macros provided by the Calimero SDK for building Rust applications. -### Prerequisites +## Core Macros -Before you begin, ensure you have Rust installed on your system. If not, follow -the official Rust installation guide for your platform: -[Rust Installation Guide](https://www.rust-lang.org/tools/install). +### #[app::state] -You should ensure you have the `wasm32-unknown-unknown` target installed. Run -the following command in your terminal to install the target: - -```bash title="Terminal" -rustup target add wasm32-unknown-unknown -``` - -### Setting Up Your Project - -To create a new project, initialize a Rust library project using Cargo. Run the -following command in your terminal: - -```bash title="Terminal" -cargo new --lib kv-store -``` - -You should have a tree that looks like this: - -```bash title="Terminal" -$ tree kv-store -kv-store -├── Cargo.toml -└── src - └── lib.rs - -2 directories, 2 files -``` - -At this point, we can `cd` into the `kv-store` directory: - -```bash title="Terminal" -cd kv-store -``` - -Next, you need to specify the crate-type as `cdylib` in your `Cargo.toml` file -to generate a dynamic library that can be compiled to Wasm: - -```toml title="File: Cargo.toml" -[lib] -crate-type = ["cdylib"] -``` - -You can now configure your project to use the Calimero SDK by adding it as a -dependency in your `Cargo.toml` file: - -```toml title="File: Cargo.toml" -[dependencies] -calimero-sdk = { git = "https://github.com/calimero-network/core" } -calimero-storage = { git = "https://github.com/calimero-network/core" } -``` - -Then, we need to specify a custom build profile for the most compact Wasm -output: - -```toml title="File: Cargo.toml" -[profile.app-release] -inherits = "release" -codegen-units = 1 -opt-level = "z" -lto = true -debug = false -panic = "abort" -overflow-checks = true -``` - -
- Your `Cargo.toml` file should now look like this - -```toml title="File: Cargo.toml" showLineNumbers -[package] -name = "kv-store" -version = "0.1.0" -edition = "2021" - -# highlight-start -[lib] -crate-type = ["cdylib"] -# highlight-end - -# highlight-start -[dependencies] -calimero-sdk = { git = "https://github.com/calimero-network/core" } -calimero-storage = { git = "https://github.com/calimero-network/core" } -# highlight-end - -# highlight-start -[profile.app-release] -inherits = "release" -codegen-units = 1 -opt-level = "z" -lto = true -debug = false -panic = "abort" -overflow-checks = true -# highlight-end -``` - -
- -And finally, create a `build.sh` script to compile your application into Wasm -format, for example: - -```bash title="File: build.sh" showLineNumbers -#!/bin/bash -set -e - -cd "$(dirname $0)" - -TARGET="${CARGO_TARGET_DIR:-../../target}" - -rustup target add wasm32-unknown-unknown - -cargo build --target wasm32-unknown-unknown --profile app-release - -mkdir -p res - -cp $TARGET/wasm32-unknown-unknown/app-release/kv_store.wasm ./res/ -``` - -You can optionally choose to install and use -[`wasm-opt`](https://github.com/WebAssembly/binaryen), for an additional -optimization step in the build script. This step is not required but can help -reduce the size of the generated Wasm file: - -```bash title="File: build.sh" -if command -v wasm-opt > /dev/null; then - wasm-opt -Oz ./res/kv_store.wasm -o ./res/kv_store.wasm -fi -``` - -Don't forget to make the `build.sh` script executable: - -```bash title="Terminal" -chmod +x build.sh -``` - -At this point, your project structure should look like this: - -```bash title="Terminal" -$ tree -. -├── Cargo.toml -├── build.sh -└── src - └── lib.rs - -2 directories, 3 files -``` - -### Writing Your Application - -Now, let's create a simple key-value store application using the Calimero SDK. -Start by defining your application logic in `lib.rs`: - -```rust title="File: src/lib.rs" showLineNumbers -use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; -use calimero_sdk::app; +Marks a struct as the application state. +The state struct must implement `BorshSerialize` and `BorshDeserialize`. +```rust #[app::state] #[derive(Default, BorshSerialize, BorshDeserialize)] #[borsh(crate = "calimero_sdk::borsh")] -struct KvStore {} - -#[app::logic] -impl KvStore { - #[app::init] - pub fn init() -> KvStore { - KvStore {} - } +struct MyAppState { + // Your state fields here } ``` -The `KvStore` struct represents the state of your application, which will be -borsh-encoded in the app-scoped state partition on the node's storage. The -`#[app::state]` attribute macro marks the struct as the application state, -permitting its use by Calimero SDK. - -The `#[app::logic]` attribute macro marks the implementation block as the -application logic, allowing you to define the methods that interact with the -application state. An initializer method (named `init`) is denoted by the -`#[app::init]` attribute macro, which is called when the application is executed -against a freshly created context. - -Consider a method like `get` that retrieves a value from the key-value store: +When emitting events, spcify the event type: ```rust -pub fn get(&self, key: &str) -> Result, Error> { - // Snip... +#[app::state(emits = for<'a> MyEvent<'a>)] +struct MyAppState { + // Your state fields here } ``` -The inputs must be deserializable from the transaction data, and the output must -be serializable to the response data. The `Option` type is used to represent the -possibility of the key not being present in the store. The `Error` type is used -to represent the possible error conditions that may occur during the execution -of the method. -And now, here's a complete example of a key-value store application: +### #[app::logic] -```rust title="File: src/lib.rs" showLineNumbers -use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; -use calimero_sdk::types::Error; -use calimero_sdk::{app, env}; -use calimero_storage::collections::UnorderedMap; - -#[app::state] -#[derive(Default, BorshSerialize, BorshDeserialize)] -#[borsh(crate = "calimero_sdk::borsh")] -struct KvStore { - entries: UnorderedMap, -} +Marks an implementation block as containing the application logic. +```rust #[app::logic] -impl KvStore { - #[app::init] - pub fn init() -> KvStore { - // highlight-start - KvStore { - items: UnorderedMap::new(), - } - // highlight-end - } - - // highlight-start - pub fn set(&mut self, key: String, value: String) -> Result<(), Error> { - env::log(&format!("Setting key: {:?} to value: {:?}", key, value)); - - self.entries.insert(key, value)?; - - Ok(()) - } - // highlight-end - - // highlight-start - pub fn entries(&self) -> Result, Error> { - env::log("Getting all entries"); - - Ok(self.items.entries()?.collect()) - } - // highlight-end - - // highlight-start - pub fn get(&self, key: &str) -> Result, Error> { - env::log(&format!("Getting key: {:?}", key)); - - self.items.get(key).map_err(Into::into) - } - // highlight-end - - // highlight-start - pub fn remove(&mut self, key: &str) -> Result, Error> { - env::log(&format!("Removing key: {:?}", key)); - - self.items.remove(key).map_err(Into::into) - } - // highlight-end +impl MyAppState { + // Your methods here } ``` -### Building Your Application - -Once your application logic is defined, run the `./build.sh` script to compile -your application into Wasm format. This script will generate `kv_store.wasm` in -the `res` folder of your application. +### #[app::init] -```bash title="Terminal" -$ ./build.sh -info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date - # Snip... - Compiling calimero-sdk v0.1.0 - Compiling calimero-storage v0.1.0 - Compiling kv-store v0.1.0 (/apps/kv-store) - Finished `app-release` profile [optimized] target(s) in 1.20s +Marks a method as the initializer, which is called when the application is first deployed. -$ tree -. -├── Cargo.toml -├── build.sh -├── res -│   └── kv_store.wasm -└── src - └── lib.rs - -3 directories, 4 files +```rust +#[app::logic] +impl MyAppState { + #[app::init] + pub fn init() -> Self { + Self::default() + } +} ``` -### Deploying Your Application +### #[app::event] -After successfully building your application, you can upload the compiled -`kv_store.wasm` to the registry for use by a live Calimero node. +Defines an event type that can be emitted by your application. -### Writing Efficient Code with Calimero SDK - -In the following code snippet: - -```rust title="File: src/lib.rs" -pub fn get(&self, key: &str) -> Result, Error> { - // Snip... +```rust +#[app::event] +pub enum MyEvent<'a> { + ValueUpdated { key: &'a str, value: &'a str }, + ValueRemoved { key: &'a str }, } ``` -you'll notice that we prioritize using references instead of owned values. This -approach optimizes performance and memory usage by minimizing unnecessary data -copying. - -For input parameters, such as `&str` and `&[u8]`, utilizing references allows -you to avoid unnecessary copying of data. Similarly, for output values, you can -return references to data that live as long as `&self` or any of the input -parameters. By doing so, you reduce memory overhead and improve the overall -efficiency of your application. - -### Handling Errors with Calimero SDK - -When designing methods that may potentially fail, it's recommended to return a -`Result` type with an error variant representing the possible failure cases. -This enables you to handle errors more effectively and communicate error -conditions to users of your application. This is recommended over using the -`Error` type exported from `calimero_sdk` and over panicking. Both of which only -return a string message. - -#### Error Report Comparison - -Let's take the following cases (all of which fail when the key does not exist); - -1. Using `calimero_sdk::types::Error`: - - This is provided for convenience, since most errors already don't implement - `Serialize`, and so they cannot be immediately returned. This first converts - the error to a string and then returns it. Which then JSON-encodes the string - representation. - - ```rust title="File: src/lib.rs" - use calimero_sdk::types::Error; - - pub fn get(&self, key: &str) -> Result { - self.items.get(key)?.ok_or_else(|| Error::msg("key not found")) - } - ``` - - This failure will result in this outcome: - - ```rust - ExecutionError([ 107, 75, 101, 121, 32, 110, 111, 116, 32, 102, 111, 117, 110, 100, 34 ]) - ``` - - which can be decoded to - - ```json - "key not found" - ``` - - This `Error` can be constructed with `?` so long as the source error - implements `std::error::Error`. - - Behaviourally similar to - [`anyhow::Error`](https://docs.rs/anyhow/latest/anyhow/struct.Error.html) or - [`eyre::Report`](https://docs.rs/eyre/latest/eyre/struct.Report.html). - -2. Using a custom error type (recommended): - - For structured error handling, we recommend defining a custom error type that - encodes all the possible error variants for that method. This allows you to - provide more context about the error condition and handle different error - scenarios more effectively. As opposed to string parsing. - - ```rust title="File: src/lib.rs" - use calimero_sdk::serde::Serialize; - - #[derive(Debug, Serialize)] - #[serde(crate = "calimero_sdk::serde")] - #[serde(tag = "kind", content = "data")] - pub enum Error<'a> { - NotFound(&'a str), - } - ``` - - ```rust title="File: src/lib.rs" - pub fn get<'a>(&self, key: &'a str) -> Result> { - // Snip... - Err(Error::NotFound(key)) - } - ``` - - This failure will result in this outcome: - - ```rust - ExecutionError([ 123, 34, 107, 105, 110, 100, 34, 58, 34, 78, 111, 116, 70, 111, 117, 110, 100, 34, 44, 34, 100, 97, 116, 97, 34, 58, 34, 116, 104, 105, 110, 103, 34, 125 ]) - ``` - - which can be decoded to - - ```json - { "kind": "NotFound", "data": "thing" } - ``` - - As will most likely be the case, you may need to work with storage errors - while you've defined a custom error type. - - In this case, you can pull in - [`thiserror`](https://docs.rs/thiserror/latest/thiserror/) to help. - - ```rust title="File: src/lib.rs" - use thiserror::Error; - - #[derive(Debug, Error, Serialize)] - #[serde(crate = "calimero_sdk::serde")] - #[serde(tag = "kind", content = "data")] - pub enum Error<'a> { - #[error("key not found: {0}")] - NotFound(&'a str), - #[error("store error: {0}")] - StoreError(#[from] StoreError), - } - ``` - - ```rust title="File: src/lib.rs" - pub fn get<'a>(&self, key: &'a str) -> Result> { - // Snip... - self.items.get(key)?.ok_or_else(|| Error::NotFound(key)) - } - ``` - - An example store error would then be represented as: - - ```rust - ExecutionError( - [ - 123, 34, 107, 105, 110, 100, 34, 58, 34, 83, 116, 111, 114, 101, 69, 114, 114, 111, 114, 34, 44, 34, 100, 97, 116, 97, 34, 58, - 34, 73, 110, 100, 101, 120, 32, 110, 111, 116, 32, 102, 111, 117, 110, 100, 32, 102, 111, 114, 32, 73, 68, 58, 32, 57, 51, 49, 53, - 97, 98, 101, 49, 101, 97, 101, 48, 102, 102, 53, 98, 48, 48, 52, 53, 51, 97, 100, 97, 102, 99, 99, 53, 102, 101, 102, 50, 49, 100, - 55, 52, 49, 51, 57, 55, 101, 50, 49, 99, 53, 49, 53, 51, 55, 99, 51, 54, 52, 52, 50, 52, 50, 48, 56, 55, 52, 57, 99, 57, 34, 125, - ], - ) - ``` - - which can be decoded to +## Event emission - ```json - { - "kind": "StoreError", - "data": "Index not found for ID: 9315abe1eae0ff5b00453adafcc5fef21d741397e21c51537c364424208749c9" - } - ``` +Use the `app::emit!` macro to emit events from your application: -3. Panic (ideally, development only) - - ```rust title="File: src/lib.rs" - pub fn get(&self, key: &str) -> String { - self.items.get(key).expect("store error").expect("key not found") - } - ``` - - A non-existent key would then lead to this outcome: - - ```rust - HostError( - Panic { - context: Guest, - message: "key not found", - location: At { - file: "apps/kv-store/src/lib.rs", - line: 98, - column: 14, - }, - }, - ) - ``` - - And a storage error, would produce this: - - ```rust - HostError( - Panic { - context: Guest, - message: "store error: StorageError(IndexNotFound(Id { bytes: [123, 240, 135, 21, 77, 143, 81, 169, 15, 202, 99, 210, 167, 165, 188, 156, 87, 146, 7, 211, 100, 92, 169, 189, 124, 115, 200, 242, 240, 73, 68, 123] }))", - location: At { - file: "apps/kv-store/src/lib.rs", - line: 98, - column: 14, - }, - }, - ) - ``` - -By following the second (recommended) approach, you can handle errors more -gracefully and provide meaningful feedback to users of your Calimero -application. - -And the first approach, if you want a hassle-free method of dealing with errors. - -### Emitting Events with Calimero SDK - -To facilitate real-time monitoring of state transitions within your Calimero -application, you can emit events using the `app::emit!` macro provided by the -Calimero SDK. Event emission is particularly useful for handling live state -transitions triggered by other actors, allowing subscribed clients to receive -immediate updates about relevant actions. +```rust +app::emit!(MyEvent::ValueUpdated { + key: &key, + value: &new_value +}); +``` -Let's focus on emitting events for mutating calls, specifically `set` and -`remove` methods: +## Complete Example -First, define your custom events using the `#[app::event]` proc macro. In this -example, we'll define events for setting a new key-value pair (`Inserted`), -updating an existing value (`Updated`), and removing a key-value pair -(`Removed`): +Here's a minimal example showing how these macros work together: -```rust title="File: src/lib.rs" -use calimero_sdk::app; +```rust +use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use calimero_sdk::{app, env}; +use calimero_storage::collections::UnorderedMap; #[app::event] -pub enum Event<'a> { - Inserted { key: &'a str, value: &'a str }, - Updated { key: &'a str, value: &'a str }, - Removed { key: &'a str }, +pub enum StoreEvent<'a> { + ValueSet { key: &'a str, value: &'a str }, } -``` -Each event variant can carry additional data to provide context about the event. - -Now, you need to associate the event with the application logic by annotating -the application state. - -```rust title="File: src/lib.rs" -// highlight-start -#[app::state(emits = for<'a> Event<'a>)] -// highlight-end +#[app::state(emits = for<'a> StoreEvent<'a>)] #[derive(Default, BorshSerialize, BorshDeserialize)] #[borsh(crate = "calimero_sdk::borsh")] -struct KvStore { - // Snip... +struct Store { + values: UnorderedMap, } -``` -And finally, within your application logic methods, emit events using the -`app::emit!` macro: +#[app::logic] +impl Store { + #[app::init] + pub fn init() -> Self { + Self { + values: UnorderedMap::new(), + } + } -```rust title="File: src/lib.rs" -pub fn set(&mut self, key: String, value: String) -> Result<(), Error> { - if self.items.contains(&key)? { - app::emit!(Event::Updated { - key: &key, - value: &value, - }); - } else { - app::emit!(Event::Inserted { - key: &key, - value: &value, + pub fn set(&mut self, key: String, value: String) { + self.values.insert(key, value).unwrap(); + app::emit!(StoreEvent::ValueSet { + key: &key, + value: &value }); } - - self.items.insert(key, value)?; - - Ok(()) -} - -pub fn remove(&mut self, key: &str) -> Result { - app::emit!(Event::Removed { key }); - - self.entries.remove(key)?.ok_or_else(|| Error::msg("key not found")) } ``` +## Important Notes -In each method, we emit the corresponding event with relevant data. This allows -external observers to react to these events and take appropriate actions. - -By emitting events, you can ensure connected clients receive real-time updates -about state transitions within your Calimero application, enabling them to -respond to changes as they occur. - -### Ensuring Atomicity and Event Reliability in Calimero Applications - -In Calimero applications, ensuring atomicity of state changes and reliability of -event emission is crucial for maintaining data consistency and facilitating -reliable communication between actors. Here's how atomicity and event -reliability are enforced: - -#### Atomic State Changes - -When a method call fails, whether due to panics or returning an `Err`, all state -changes made up to that point are discarded. This ensures that if an operation -cannot be completed successfully, the application's state remains consistent and -unaffected by partial updates. By enforcing atomicity, Calimero promotes data -integrity and prevents inconsistencies that may arise from incomplete -transactions. - -#### Reliable Event Emission - -Similarly, event emission in Calimero applications is tied to the successful -execution of transactions. Events are only relayed when a transaction has been -successfully executed, ensuring that external observers receive updates about -state changes reliably. By linking event emission to transaction execution, -Calimero guarantees that event notifications accurately reflect the -application's current state, enhancing the reliability and consistency of -communication between actors. - -This also means it doesn't matter if the event emission is done before or after -the state change, as the event will only be emitted if the state change is -successful. - -By adhering to these principles of atomicity and event reliability, Calimero -applications maintain data integrity and enable robust interaction between -different components, facilitating the development of secure and dependable -decentralized systems. - -### Local-First Efficiency: No Network Overhead for Read-Only Calls - -In Calimero, adherence to the local-first principle eliminates the need for -network communication in read-only calls. Since read-only operations don't -modify the state, there's no associated network overhead. This local-first -approach streamlines data access, promoting efficient and responsive application -performance without unnecessary network activity. - -### Conclusion - -You've now learned how to set up a Rust project using the Calimero SDK, write a -simple application, build it into Wasm, and prepare it for deployment. -Experiment with different features and functionalities to create powerful and -secure applications with Calimero. +
    +
  1. State changes are atomic - if a method fails, all changes are rolled back
  2. +
  3. Events are only emitted if the transaction succeeds
  4. +
  5. Read-only operations have no network overhead
  6. +
  7. All public methods in the `#[app::logic]` block become available as application endpoints
  8. +
- +For a detailed guide on building a complete application using these macros, +see our [Tutorial](../../../../tutorials/build-app) Guide on building a Key Value Store. -Happy coding! 🚀 diff --git a/docs/06-tutorials/04-build-app.mdx b/docs/06-tutorials/04-build-app.mdx new file mode 100644 index 00000000..09d89aa9 --- /dev/null +++ b/docs/06-tutorials/04-build-app.mdx @@ -0,0 +1,638 @@ +--- +id: build-app +title: Key-Value Store tutorial +--- + +## Building a Key-Value Store with Calimero SDK + +The Calimero SDK for Rust empowers developers to build applications that compile +to WebAssembly (Wasm) and run securely within the Calimero virtual machine. +This guide will walk you through creating a complete key-value store application +and preparing it for deployment. + +### Prerequisites + +Before you begin, ensure you have Rust installed on your system. If not, follow +the official Rust installation guide for your platform: +[Rust Installation Guide](https://www.rust-lang.org/tools/install). + +You should ensure you have the `wasm32-unknown-unknown` target installed. Run +the following command in your terminal to install the target: + +```bash title="Terminal" +rustup target add wasm32-unknown-unknown +``` + +### Setting Up Your Project + +To create a new project, initialize a Rust library project using Cargo. Run the +following command in your terminal: + +```bash title="Terminal" +cargo new --lib kv-store +``` + +You should have a tree that looks like this: + +```bash title="Terminal" +$ tree kv-store +kv-store +├── Cargo.toml +└── src + └── lib.rs + +2 directories, 2 files +``` + +At this point, we can `cd` into the `kv-store` directory: + +```bash title="Terminal" +cd kv-store +``` + +Next, you need to specify the crate-type as `cdylib` in your `Cargo.toml` file +to generate a dynamic library that can be compiled to Wasm: + +```toml title="File: Cargo.toml" +[lib] +crate-type = ["cdylib"] +``` + +You can now configure your project to use the Calimero SDK by adding it as a +dependency in your `Cargo.toml` file: + +```toml title="File: Cargo.toml" +[dependencies] +calimero-sdk = { git = "https://github.com/calimero-network/core" } +calimero-storage = { git = "https://github.com/calimero-network/core" } +``` + +Then, we need to specify a custom build profile for the most compact Wasm +output: + +```toml title="File: Cargo.toml" +[profile.app-release] +inherits = "release" +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = true +``` + +
+ Your `Cargo.toml` file should now look like this + +```toml title="File: Cargo.toml" showLineNumbers +[package] +name = "kv-store" +version = "0.1.0" +edition = "2021" + +# highlight-start +[lib] +crate-type = ["cdylib"] +# highlight-end + +# highlight-start +[dependencies] +calimero-sdk = { git = "https://github.com/calimero-network/core" } +calimero-storage = { git = "https://github.com/calimero-network/core" } +# highlight-end + +# highlight-start +[profile.app-release] +inherits = "release" +codegen-units = 1 +opt-level = "z" +lto = true +debug = false +panic = "abort" +overflow-checks = true +# highlight-end +``` + +
+ +And finally, create a `build.sh` script to compile your application into Wasm +format, for example: + +```bash title="File: build.sh" showLineNumbers +#!/bin/bash +set -e + +cd "$(dirname $0)" + +TARGET="${CARGO_TARGET_DIR:-../../target}" + +rustup target add wasm32-unknown-unknown + +cargo build --target wasm32-unknown-unknown --profile app-release + +mkdir -p res + +cp $TARGET/wasm32-unknown-unknown/app-release/kv_store.wasm ./res/ +``` + +You can optionally choose to install and use +[`wasm-opt`](https://github.com/WebAssembly/binaryen), for an additional +optimization step in the build script. This step is not required but can help +reduce the size of the generated Wasm file: + +```bash title="File: build.sh" +if command -v wasm-opt > /dev/null; then + wasm-opt -Oz ./res/kv_store.wasm -o ./res/kv_store.wasm +fi +``` + +Don't forget to make the `build.sh` script executable: + +```bash title="Terminal" +chmod +x build.sh +``` + +At this point, your project structure should look like this: + +```bash title="Terminal" +$ tree +. +├── Cargo.toml +├── build.sh +└── src + └── lib.rs + +2 directories, 3 files +``` + +### Writing Your Application + +Now, let's create a simple key-value store application using the Calimero SDK. +Start by defining your application logic in `lib.rs`: + +```rust title="File: src/lib.rs" showLineNumbers +use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use calimero_sdk::app; + +#[app::state] +#[derive(Default, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "calimero_sdk::borsh")] +struct KvStore {} + +#[app::logic] +impl KvStore { + #[app::init] + pub fn init() -> KvStore { + KvStore {} + } +} +``` + +The `KvStore` struct represents the state of your application, which will be +borsh-encoded in the app-scoped state partition on the node's storage. The +`#[app::state]` attribute macro marks the struct as the application state, +permitting its use by Calimero SDK. + +The `#[app::logic]` attribute macro marks the implementation block as the +application logic, allowing you to define the methods that interact with the +application state. An initializer method (named `init`) is denoted by the +`#[app::init]` attribute macro, which is called when the application is executed +against a freshly created context. + +Consider a method like `get` that retrieves a value from the key-value store: + +```rust +pub fn get(&self, key: &str) -> Result, Error> { + // Snip... +} +``` + +The inputs must be deserializable from the transaction data, and the output must +be serializable to the response data. The `Option` type is used to represent the +possibility of the key not being present in the store. The `Error` type is used +to represent the possible error conditions that may occur during the execution +of the method. + +And now, here's a complete example of a key-value store application: + +```rust title="File: src/lib.rs" showLineNumbers +use calimero_sdk::borsh::{BorshDeserialize, BorshSerialize}; +use calimero_sdk::types::Error; +use calimero_sdk::{app, env}; +use calimero_storage::collections::UnorderedMap; + +#[app::state] +#[derive(Default, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "calimero_sdk::borsh")] +struct KvStore { + entries: UnorderedMap, +} + +#[app::logic] +impl KvStore { + #[app::init] + pub fn init() -> KvStore { + // highlight-start + KvStore { + items: UnorderedMap::new(), + } + // highlight-end + } + + // highlight-start + pub fn set(&mut self, key: String, value: String) -> Result<(), Error> { + env::log(&format!("Setting key: {:?} to value: {:?}", key, value)); + + self.entries.insert(key, value)?; + + Ok(()) + } + // highlight-end + + // highlight-start + pub fn entries(&self) -> Result, Error> { + env::log("Getting all entries"); + + Ok(self.items.entries()?.collect()) + } + // highlight-end + + // highlight-start + pub fn get(&self, key: &str) -> Result, Error> { + env::log(&format!("Getting key: {:?}", key)); + + self.items.get(key).map_err(Into::into) + } + // highlight-end + + // highlight-start + pub fn remove(&mut self, key: &str) -> Result, Error> { + env::log(&format!("Removing key: {:?}", key)); + + self.items.remove(key).map_err(Into::into) + } + // highlight-end +} +``` + +### Building Your Application + +Once your application logic is defined, run the `./build.sh` script to compile +your application into Wasm format. This script will generate `kv_store.wasm` in +the `res` folder of your application. + +```bash title="Terminal" +$ ./build.sh +info: component 'rust-std' for target 'wasm32-unknown-unknown' is up to date + # Snip... + Compiling calimero-sdk v0.1.0 + Compiling calimero-storage v0.1.0 + Compiling kv-store v0.1.0 (/apps/kv-store) + Finished `app-release` profile [optimized] target(s) in 1.20s + +$ tree +. +├── Cargo.toml +├── build.sh +├── res +│   └── kv_store.wasm +└── src + └── lib.rs + +3 directories, 4 files +``` + +### Deploying Your Application + +After successfully building your application, you can upload the compiled +`kv_store.wasm` to the registry for use by a live Calimero node. + +### Writing Efficient Code with Calimero SDK + +In the following code snippet: + +```rust title="File: src/lib.rs" +pub fn get(&self, key: &str) -> Result, Error> { + // Snip... +} +``` + +you'll notice that we prioritize using references instead of owned values. This +approach optimizes performance and memory usage by minimizing unnecessary data +copying. + +For input parameters, such as `&str` and `&[u8]`, utilizing references allows +you to avoid unnecessary copying of data. Similarly, for output values, you can +return references to data that live as long as `&self` or any of the input +parameters. By doing so, you reduce memory overhead and improve the overall +efficiency of your application. + +### Handling Errors with Calimero SDK + +When designing methods that may potentially fail, it's recommended to return a +`Result` type with an error variant representing the possible failure cases. +This enables you to handle errors more effectively and communicate error +conditions to users of your application. This is recommended over using the +`Error` type exported from `calimero_sdk` and over panicking. Both of which only +return a string message. + +#### Error Report Comparison + +Let's take the following cases (all of which fail when the key does not exist); + +1. Using `calimero_sdk::types::Error`: + + This is provided for convenience, since most errors already don't implement + `Serialize`, and so they cannot be immediately returned. This first converts + the error to a string and then returns it. Which then JSON-encodes the string + representation. + + ```rust title="File: src/lib.rs" + use calimero_sdk::types::Error; + + pub fn get(&self, key: &str) -> Result { + self.items.get(key)?.ok_or_else(|| Error::msg("key not found")) + } + ``` + + This failure will result in this outcome: + + ```rust + ExecutionError([ 107, 75, 101, 121, 32, 110, 111, 116, 32, 102, 111, 117, 110, 100, 34 ]) + ``` + + which can be decoded to + + ```json + "key not found" + ``` + + This `Error` can be constructed with `?` so long as the source error + implements `std::error::Error`. + + Behaviourally similar to + [`anyhow::Error`](https://docs.rs/anyhow/latest/anyhow/struct.Error.html) or + [`eyre::Report`](https://docs.rs/eyre/latest/eyre/struct.Report.html). + +2. Using a custom error type (recommended): + + For structured error handling, we recommend defining a custom error type that + encodes all the possible error variants for that method. This allows you to + provide more context about the error condition and handle different error + scenarios more effectively. As opposed to string parsing. + + ```rust title="File: src/lib.rs" + use calimero_sdk::serde::Serialize; + + #[derive(Debug, Serialize)] + #[serde(crate = "calimero_sdk::serde")] + #[serde(tag = "kind", content = "data")] + pub enum Error<'a> { + NotFound(&'a str), + } + ``` + + ```rust title="File: src/lib.rs" + pub fn get<'a>(&self, key: &'a str) -> Result> { + // Snip... + Err(Error::NotFound(key)) + } + ``` + + This failure will result in this outcome: + + ```rust + ExecutionError([ 123, 34, 107, 105, 110, 100, 34, 58, 34, 78, 111, 116, 70, 111, 117, 110, 100, 34, 44, 34, 100, 97, 116, 97, 34, 58, 34, 116, 104, 105, 110, 103, 34, 125 ]) + ``` + + which can be decoded to + + ```json + { "kind": "NotFound", "data": "thing" } + ``` + + As will most likely be the case, you may need to work with storage errors + while you've defined a custom error type. + + In this case, you can pull in + [`thiserror`](https://docs.rs/thiserror/latest/thiserror/) to help. + + ```rust title="File: src/lib.rs" + use thiserror::Error; + + #[derive(Debug, Error, Serialize)] + #[serde(crate = "calimero_sdk::serde")] + #[serde(tag = "kind", content = "data")] + pub enum Error<'a> { + #[error("key not found: {0}")] + NotFound(&'a str), + #[error("store error: {0}")] + StoreError(#[from] StoreError), + } + ``` + + ```rust title="File: src/lib.rs" + pub fn get<'a>(&self, key: &'a str) -> Result> { + // Snip... + self.items.get(key)?.ok_or_else(|| Error::NotFound(key)) + } + ``` + + An example store error would then be represented as: + + ```rust + ExecutionError( + [ + 123, 34, 107, 105, 110, 100, 34, 58, 34, 83, 116, 111, 114, 101, 69, 114, 114, 111, 114, 34, 44, 34, 100, 97, 116, 97, 34, 58, + 34, 73, 110, 100, 101, 120, 32, 110, 111, 116, 32, 102, 111, 117, 110, 100, 32, 102, 111, 114, 32, 73, 68, 58, 32, 57, 51, 49, 53, + 97, 98, 101, 49, 101, 97, 101, 48, 102, 102, 53, 98, 48, 48, 52, 53, 51, 97, 100, 97, 102, 99, 99, 53, 102, 101, 102, 50, 49, 100, + 55, 52, 49, 51, 57, 55, 101, 50, 49, 99, 53, 49, 53, 51, 55, 99, 51, 54, 52, 52, 50, 52, 50, 48, 56, 55, 52, 57, 99, 57, 34, 125, + ], + ) + ``` + + which can be decoded to + + ```json + { + "kind": "StoreError", + "data": "Index not found for ID: 9315abe1eae0ff5b00453adafcc5fef21d741397e21c51537c364424208749c9" + } + ``` + +3. Panic (ideally, development only) + + ```rust title="File: src/lib.rs" + pub fn get(&self, key: &str) -> String { + self.items.get(key).expect("store error").expect("key not found") + } + ``` + + A non-existent key would then lead to this outcome: + + ```rust + HostError( + Panic { + context: Guest, + message: "key not found", + location: At { + file: "apps/kv-store/src/lib.rs", + line: 98, + column: 14, + }, + }, + ) + ``` + + And a storage error, would produce this: + + ```rust + HostError( + Panic { + context: Guest, + message: "store error: StorageError(IndexNotFound(Id { bytes: [123, 240, 135, 21, 77, 143, 81, 169, 15, 202, 99, 210, 167, 165, 188, 156, 87, 146, 7, 211, 100, 92, 169, 189, 124, 115, 200, 242, 240, 73, 68, 123] }))", + location: At { + file: "apps/kv-store/src/lib.rs", + line: 98, + column: 14, + }, + }, + ) + ``` + +By following the second (recommended) approach, you can handle errors more +gracefully and provide meaningful feedback to users of your Calimero +application. + +And the first approach, if you want a hassle-free method of dealing with errors. + +### Emitting Events with Calimero SDK + +To facilitate real-time monitoring of state transitions within your Calimero +application, you can emit events using the `app::emit!` macro provided by the +Calimero SDK. Event emission is particularly useful for handling live state +transitions triggered by other actors, allowing subscribed clients to receive +immediate updates about relevant actions. + +Let's focus on emitting events for mutating calls, specifically `set` and +`remove` methods: + +First, define your custom events using the `#[app::event]` proc macro. In this +example, we'll define events for setting a new key-value pair (`Inserted`), +updating an existing value (`Updated`), and removing a key-value pair +(`Removed`): + +```rust title="File: src/lib.rs" +use calimero_sdk::app; + +#[app::event] +pub enum Event<'a> { + Inserted { key: &'a str, value: &'a str }, + Updated { key: &'a str, value: &'a str }, + Removed { key: &'a str }, +} +``` + +Each event variant can carry additional data to provide context about the event. + +Now, you need to associate the event with the application logic by annotating +the application state. + +```rust title="File: src/lib.rs" +// highlight-start +#[app::state(emits = for<'a> Event<'a>)] +// highlight-end +#[derive(Default, BorshSerialize, BorshDeserialize)] +#[borsh(crate = "calimero_sdk::borsh")] +struct KvStore { + // Snip... +} +``` + +And finally, within your application logic methods, emit events using the +`app::emit!` macro: + +```rust title="File: src/lib.rs" +pub fn set(&mut self, key: String, value: String) -> Result<(), Error> { + if self.items.contains(&key)? { + app::emit!(Event::Updated { + key: &key, + value: &value, + }); + } else { + app::emit!(Event::Inserted { + key: &key, + value: &value, + }); + } + + self.items.insert(key, value)?; + + Ok(()) +} + +pub fn remove(&mut self, key: &str) -> Result { + app::emit!(Event::Removed { key }); + + self.entries.remove(key)?.ok_or_else(|| Error::msg("key not found")) +} +``` + +In each method, we emit the corresponding event with relevant data. This allows +external observers to react to these events and take appropriate actions. + +By emitting events, you can ensure connected clients receive real-time updates +about state transitions within your Calimero application, enabling them to +respond to changes as they occur. + +### Ensuring Atomicity and Event Reliability in Calimero Applications + +In Calimero applications, ensuring atomicity of state changes and reliability of +event emission is crucial for maintaining data consistency and facilitating +reliable communication between actors. Here's how atomicity and event +reliability are enforced: + +#### Atomic State Changes + +When a method call fails, whether due to panics or returning an `Err`, all state +changes made up to that point are discarded. This ensures that if an operation +cannot be completed successfully, the application's state remains consistent and +unaffected by partial updates. By enforcing atomicity, Calimero promotes data +integrity and prevents inconsistencies that may arise from incomplete +transactions. + +#### Reliable Event Emission + +Similarly, event emission in Calimero applications is tied to the successful +execution of transactions. Events are only relayed when a transaction has been +successfully executed, ensuring that external observers receive updates about +state changes reliably. By linking event emission to transaction execution, +Calimero guarantees that event notifications accurately reflect the +application's current state, enhancing the reliability and consistency of +communication between actors. + +This also means it doesn't matter if the event emission is done before or after +the state change, as the event will only be emitted if the state change is +successful. + +By adhering to these principles of atomicity and event reliability, Calimero +applications maintain data integrity and enable robust interaction between +different components, facilitating the development of secure and dependable +decentralized systems. + +### Local-First Efficiency + +Read-only operations (like `get`) have no network overhead, as they don't modify state and can be executed locally. + +### Conclusion + +You've now learned how to set up a Rust project using the Calimero SDK, write a +simple application, build it into Wasm, and prepare it for deployment. +Experiment with different features and functionalities to create powerful and +secure applications with Calimero. + + + +Happy coding! 🚀 diff --git a/docs/06-tutorials/_04-build-app.mdx b/docs/06-tutorials/_04-build-app.mdx deleted file mode 100644 index 9ef178dc..00000000 --- a/docs/06-tutorials/_04-build-app.mdx +++ /dev/null @@ -1,6 +0,0 @@ ---- -id: build-app -title: Build app ---- - -## Building an Application