From 74e600d2aed8c32b3bec2193c95c87aef3e63bf5 Mon Sep 17 00:00:00 2001 From: Bianca Ialangi Date: Fri, 3 Jan 2025 18:40:40 +0200 Subject: [PATCH 1/9] steps: prepare the workspace, write the code, build --- docs/developers/tutorials/crowdfunding-p1.md | 177 ++++++++++--------- 1 file changed, 96 insertions(+), 81 deletions(-) diff --git a/docs/developers/tutorials/crowdfunding-p1.md b/docs/developers/tutorials/crowdfunding-p1.md index 8bfe983bd..6412c5071 100644 --- a/docs/developers/tutorials/crowdfunding-p1.md +++ b/docs/developers/tutorials/crowdfunding-p1.md @@ -1,45 +1,45 @@ --- id: crowdfunding-p1 -title: The Crowdfunding Smart Contract (part 1) +title: The Crowdfunding Smart Contract --- [comment]: # (mx-abstract) -Write, build and deploy a simple smart contract written in Rust +Write, build and deploy a simple smart contract written in Rust. This tutorial will guide you through the process of writing, building and deploying a very simple smart contract for the MultiversX Network, written in Rust. :::important -The MultiversX Network supports smart contracts written in any programming language, but they must be compiled to WebAssembly. +The MultiversX Network supports smart contracts written in any programming language that is compiled to WebAssembly. ::: [comment]: # (mx-context-auto) -## **Introduction** +## Introduction -Let's say you need to raise EGLD for a cause that you believe in. They will obviously be well spent, but you need to get the EGLD first. For this reason, you decided to run a crowdfunding campaign on the MultiversX Network, which naturally means that you'll use a smart contract for the campaign. This tutorial will teach you how to do just that: write a crowdfunding smart contract, how to deploy it and how to use it. +Let's say you need to raise EGLD for a cause you believe in. They will obviously be well spent, but you need to get the EGLD first. For this reason, you decided to run a crowdfunding campaign on the MultiversX Network, which naturally means that you will use a smart contract for the campaign. This tutorial will teach you how to do just that: **write a crowdfunding smart contract, deploy it, and use it**. -The idea is simple: the smart contract will accept transfers until a deadline is reached, and it will keep track of all the people who sent EGLD. +The idea is simple: the smart contract will accept transfers until a deadline is reached, tracking all contributors. -If the deadline is reached and the smart contract has gathered an amount of EGLD above the desired funds, then the smart contract will consider the crowdfunding a success, and it will consequently send all the EGLD to a predetermined account (you!). +If the deadline is reached and the smart contract has gathered an amount of EGLD above the desired funds, then the smart contract will consider the crowdfunding a success and it will consequently send all the EGLD to a predetermined account (yours!). -But if the total amount of EGLD is lower than the desired target, all the donated EGLD must be sent back to the people who donated. +However, if the donations fall short of the target, the contract will return all the EGLD to the donors. [comment]: # (mx-context-auto) -## **Design** +## Design -Here's how the smart contract is designed: - -- It will have an `init` method, which is automatically executed upon deployment. This method must receive from you the information on (1) the target amount of EGLD and (2) the crowdfunding deadline, expressed as a block nonce. -- It will have a `fund` method, which people will call to send money to the smart contract. This method will receive the EGLD and will have to save all the information needed to return the EGLD in case the campaign does not reach the target. -- It will have a `claim` method. If anyone calls this method _before_ the deadline, it will do nothing and return an error. But if called _after_ the deadline, it will do one of the following: - - When you call it, and the target amount has been reached, it will send all the EGLD to you. If the amount has not been reached, it will do nothing and return an error. - - When one of the donors calls it, and the target amount has been reached, it will do nothing and return an error. But if the amount has not been reached (the campaign failed), then the smart contract will send the correct amount of EGLD back to the donor. - - When anyone else calls it, the method will do nothing and will return an error. -- It will have a `status` method, which will return information about the campaign, such as whether the campaign is ongoing or it has ended, and how much EGLD has been donated so far. You will probably call this method often, out of impatience. - -Four methods, then: `init`, `fund`, `claim` and `status`. +Here is how the smart contract methods is designed: +- `init`: automatically triggered when the contract is deployed. It takes two inputs from you: + 1. the target amount of EGLD you want to raise + 2. the crowdfunding deadline, which is expressed as a block nonce. +- `fund`: used by donors to contribute EGLD to the campaign. It will receive EGLD and save the necessary details so the contract can return funds if the campaign doesn't reach its goal. +- `claim`: if called before the deadline, it does nothing and returns an error. If called after the deadline: + - By you (the campaign creator), it sends all the raised EGLD to your account if the target amount is met. Otherwise, it returns an error. + - By a donor, it refunds their contribution if the target amount isn’t reached. If the target is met, it does nothing and returns an error. + - By anyone else, it does nothing and returns an error. +- `status`: Provides information about the campaign, such as whether it is still active or completed and how much EGLD has been raised so far. You will likely use this frequently to monitor progress. This tutorial will firstly focus on the `init` method, to get you acquainted with the development process and tools. You will implement `init` and also _write unit tests_ for it. +In this part of the tutorial, we will start with the `init` method to familiarize you with the development process and tools. You wll not only implement the init method but also **create unit tests** to ensure it works as expected. :::note testing Automated testing is exceptionally important for the development of smart contracts, due to the sensitive nature of the information they must handle. @@ -47,10 +47,12 @@ Automated testing is exceptionally important for the development of smart contra [comment]: # (mx-context-auto) -## Prerequisites +## Prerequisites TODO! [comment]: # (mx-context-auto) +### sc-meta TODO + ### Rust Install **Rust** and [**sc-meta**](/developers/meta/sc-meta) as depicted [here](/sdk-and-tools/troubleshooting/rust-setup). @@ -65,92 +67,100 @@ For contract developers, we generally recommend [**VSCode**](https://code.visual [comment]: # (mx-context-auto) -## **Step 1: prepare the workspace** +## Step 1: prepare the workspace -The source code of each smart contract requires its own folder. You'll need to create one for the crowdfunding smart contract presented here. Run these commands below in a terminal to create it: +The source code of each smart contract requires its own folder. We will start the development of the **crowdfunding** contract from the **Empty** template. To get the development environment ready, simply run the following commands in your terminal: ```bash mkdir -p ~/MultiversX/SmartContracts cd ~/MultiversX/SmartContracts sc-meta new --name crowdfunding --template empty -code crowdfunding ``` You may choose any location you want for your smart contract. The above is just an example. Either way, now that you are in the folder dedicated to the smart contract, we can begin. -Straight away you get a project that works - `sc-meta` created your project out of a template. These templates are contracts written and tested by MultiversX, which can be used by anybody as starting points. - -The last line also opens the new project in a new VS Code instance. +`sc-meta` created your project out of a template. These templates are contracts written and tested by MultiversX, which can be used by anybody as starting points. -Let's inspect the file `Cargo.toml`: +```toml title=Cargo.toml +[package] +name = "crowdfunding" +version = "0.0.0" +publish = false +edition = "2021" +authors = ["you"] -- The package is unsurprisingly named `crowdfunding`, and has the version `0.0.1`. You can set any version you like, just make sure it has 3 numbers separated by dots. It's a requirement. -- This package has dependencies. It will require other packages. Since you're writing a Rust smart contract for the MultiversX Network, you'll need a few special and very helpful packages, developed by MultiversX. -- The file `src/crowdfunding.rs` will contain the source code of the smart contract, and that is what the `[lib]` section is declaring. You can name this file anything you want. The default Rust naming is `lib.rs`, but it can be easier organizing your code when the main code files bear the names of the contracts. -- The resulting binary will be named `crowdfunding` (actually, `crowdfunding.wasm`, but the compiler will add the `.wasm` part), based on the crate name. +[lib] +path = "src/crowdfunding.rs" -[comment]: # (mx-context-auto) - -## **Step 2: write the code** +[dependencies.multiversx-sc] +version = "0.54.6" -With the structure in place, you can now write the code and build it. Open `src/crowdfunding.rs` , remove the existing `Empty` code and insert the following: +[dev-dependencies] +num-bigint = "0.4" -```rust title=hello-world.rs -#![no_std] +[dev-dependencies.multiversx-sc-scenario] +version = "0.54.6" -multiversx_sc::imports!(); - -#[multiversx_sc::contract] -pub trait Crowdfunding { - #[init] - fn init(&self) { - } -} +[workspace] +members = [ + ".", + "meta", +] ``` -Let's take a look at the code. The first three lines declare some characteristics of the code. You don't need to understand them (just skip ahead if you will), but here are some explanations: - -- `no_std` means that the smart contract **has no access to standard libraries**. That might sound restrictive, but the trade-off is that the code will be lean and very light. It is entirely possible to create a smart contract with the standard libraries, but that would add a lot of overhead, and is not recommended. Definitely not needed for the Crowdfunding smart contract. +Let's inspect the file found at path `~/MultiversX/SmartContracts/crowdfunding/Cargo.toml`: +- The `[package]` represent the **project** which is unsurprisingly named `crowdfunding`, and has the version `0.0.0`. You can set any version you like, just make sure it has 3 numbers separated by dots. It is a requirement. The `publish` is set to **false** to prevent the package from being published to Rust’s central package registry, crates.io. It's useful for private or experimental projects. +- `[lib]` declares the source code of the smart contracts, which in our case is `src/crowdfunding.rs`. You can name this file anything you want. The default Rust naming is `lib.rs`, but it can be easier organizing your code when the main code files bear the names of the contracts. +- This project has `dependencies` and `dev-dependencies`. You'll need a few special and very helpful packages: + - `multiversx-sc`: developed by MultiversX, it is the interface that the smart contract sees and can use. + - `multiversx-sc-scenario`: developed by MultiversX, it is the interface that defines and runs blockchain scenarios involving smart contracts. + - `num-bigint`: for working with arbitrarily large integers. +- `[workspace]` is a group of related Rust projects that share common dependencies or build settings. +- The resulting binary will be the name of the project, which in our case is `crowdfunding` (actually, `crowdfunding.wasm`, but the compiler will add the `.wasm` part). [comment]: # (mx-context-auto) -### **Bring in the framework** +## Step 2: write the code -The 3rd line contains the command `multiversx_sc::imports!();`. This command imports the dependencies we mentioned when we discussed the `Cargo.toml` file. It effectively grants you access to the MultiversX framework for Rust smart contracts, which is designed to simplify the code enormously. +With the structure in place, you can now write the code and build it. -The framework itself is a topic for another day, but you should be aware that smart contracts written in Rust aren't normally this simple. It's the framework that does the heavy lifting, so that your code stays clean and readable. Line 5 is your first contact with the framework: +Open `src/crowdfunding.rs`: ```rust -#[multiversx_sc::contract] -``` - -This line simply tells the framework to treat the next `trait` declaration (we'll get to it in a moment) as a smart contract. Because of this line, the framework will _automatically generate_ much of the code required. You won't see the generated code now (but you can). - -[comment]: # (mx-context-auto) - -### **Make it a trait** +#![no_std] // [1] -Your smart contract effectively starts at line 9. We could have gotten here quicker, but you wanted to know what the code means, and it took a little while to explain. We're finally here, though. Let's look at the code again: +#[allow(unused_imports)] // [2] +use multiversx_sc::imports::*; // [3] -It helps to know what a trait is in Rust, before continuing (the [Rust book explains it well](https://doc.rust-lang.org/book/ch10-02-traits.html)). +/// An empty contract. To be used as a template when starting a new contract from scratch. +#[multiversx_sc::contract] // [4] +pub trait Crowdfunding { // [5] + #[init] // [6] + fn init(&self) {} // [7] -For now, you only need to remember that you write your smart contract as the `trait Crowdfunding`, in order to allow the MultiversX framework to generate the support code for you, resulting in a hidden `struct CrowdfundingImpl`. - -[comment]: # (mx-context-auto) + #[upgrade] // [8] + fn upgrade(&self) {} // [9] +} +``` -### **Init** +Let's take a look at the code: -Every smart contract must define a constructor method, which is run _once and only once_, upon deployment on the network. You can name it any way you wish, but it must be annotated with `#[init]` . The Crowdfunding smart contract needs to store some initial configuration, which will be read during subsequent calls to the other methods (these other methods are `fund`, `claim` and `status`, to refresh your memory). +- **[1]**: means that the smart contract **has no access to standard libraries**. That will make the code lean and very light. +- **[2]**: brings imports module from the the multiversx_sc crate into **crowdfunding** contract. It effectively grants you access to the [MultiversX framework for Rust smart contracts](https://github.com/multiversx/mx-sdk-rs), which is designed to simplify the code **enormously**. +- **[3]**: since the contract is still in an early stage of development, clippy (Rust's linter) will flag some imports as unused. For now, we will ignore this kind of errors. +- **[4]**: processes the **Crowdfunding** trait definition as a **smart contract** that can be deployed on the MultiversX blockchain. +- **[5]**: the contract [trait](https://doc.rust-lang.org/book/ch10-02-traits.html) where all the endpoints will be developed. +- **[6]**: marks the following method (`init`) as the constructor function for the contract. +- **[7]**: this is the constructor itself. It receives the contract's instance as parameter (_&self_). The method is called once the contract is deployed on the MultiversX blockchain. You can name it any way you wish, but it must be annotated with `#[init]`. For the moment, no initialization logic is defined. +- **[8]**: marks the following method (`upgrade`) as the upgrade function for the contract. It is called when the contract is re-deployed to the same address. +- **[9]**: this is the upgrade method itself. Similar to [7], it takes a reference to the contract instance (_&self_) and performs no specific logic here. -The `init` method of the Crowdfunding smart contract is currently empty. We'll add the actual code later. First, you want to build the whole project, to make sure everything has worked well so far, even if the smart contract does nothing right now. [comment]: # (mx-context-auto) -## **Step 3: the build** +## Step 3: build -After creating the file `src/crowdfunding.rs` with the content described in [the previous step](/developers/tutorials/crowdfunding-p1#step-2-write-the-code), you can issue the first build command. Make sure you save the file first. - -Now go back to the terminal, make sure the current folder is the one containing the Crowdfunding smart contract (use `pwd` for that), then issue the build command: +Now go back to the terminal, make sure the current folder is the one containing the Crowdfunding smart contract (`~/MultiversX/SmartContracts/crowdfunding`), then issue the **build** command: ```bash sc-meta all build @@ -158,15 +168,19 @@ sc-meta all build If this is the first time you build a Rust smart contract with the `sc-meta` command, it will take a little while before it's done. Subsequent builds will be much faster. -When the command completes, a new folder will appear: `output`. This folder now contains two files: `crowdfunding.abi.json` and `crowdfunding.wasm`. We won't be doing anything with these files just yet - wait until we get to the deployment part. Along with `output`, there are a few other folders and files generated. You can safely ignore them for now, but do not delete the `wasm` folder - it's what makes the build command faster after the initial run. +When the command completes, a new folder will appear: `/output`. This folder contains: +1. `crowdfunding.abi.json` +2. `crowdfunding.imports.json` +3. `crowdfunding.mxsc.json` +4. `crowdfunding.wasm` -The following can be safely deleted, as they are not important for this contract: +We won't be doing anything with these files just yet - wait until we get to the deployment part. Along with `/output`, there are a few other folders and files generated. You can safely ignore them for now, but do not delete the `/wasm` folder - it's what makes the build command faster after the initial run. -- the `tests` folder +`/tests` can be safely deleted, as they are not important for this contract: -The structure of your folder should be like this (output printed by the command `tree -L 3`): +The structure of your folder should be like this (output printed using command `tree -L 3`): -```text +```bash . ├── Cargo.toml ├── meta @@ -186,17 +200,18 @@ The structure of your folder should be like this (output printed by the command └── lib.rs ``` -It's time to add some functionality to the `init` function now, because the next step will take you through a very important process: testing your smart contract. +It's time to add some functionality to the `init` function now. [comment]: # (mx-context-auto) -## **Step 4: the test** +## Step 4: extend init -In this step you will use the `init` method to persist some values in the storage of the Crowdfunding smart contract. Afterwards, we will write a test to make sure that these values were properly stored. +In this step you will use the `init` method to persist some values in the storage of the Crowdfunding smart contract. + [comment]: # (mx-context-auto) -### **Storage mappers** +### Storage mappers Every smart contract is allowed to store key-value pairs into a persistent structure, created for the smart contract at the moment of its deployment on the MultiversX Network. From 7cfba538ce00506b7b90c5af2525cabaa0f8ae7e Mon Sep 17 00:00:00 2001 From: Bianca Ialangi Date: Fri, 17 Jan 2025 09:43:27 +0200 Subject: [PATCH 2/9] update crowdfunding p1 --- docs/developers/tutorials/crowdfunding-p1.md | 540 ++++++++++--------- 1 file changed, 280 insertions(+), 260 deletions(-) diff --git a/docs/developers/tutorials/crowdfunding-p1.md b/docs/developers/tutorials/crowdfunding-p1.md index 6412c5071..d4d6c1df1 100644 --- a/docs/developers/tutorials/crowdfunding-p1.md +++ b/docs/developers/tutorials/crowdfunding-p1.md @@ -5,61 +5,54 @@ title: The Crowdfunding Smart Contract [comment]: # (mx-abstract) Write, build and deploy a simple smart contract written in Rust. -This tutorial will guide you through the process of writing, building and deploying a very simple smart contract for the MultiversX Network, written in Rust. +This tutorial will guide you through the process of writing, building and deploying a simple smart contract for the MultiversX Network, written in Rust. :::important -The MultiversX Network supports smart contracts written in any programming language that is compiled to WebAssembly. +The MultiversX Network supports smart contracts written in any programming language compiled into WebAssembly. ::: [comment]: # (mx-context-auto) -## Introduction +## Scenario Let's say you need to raise EGLD for a cause you believe in. They will obviously be well spent, but you need to get the EGLD first. For this reason, you decided to run a crowdfunding campaign on the MultiversX Network, which naturally means that you will use a smart contract for the campaign. This tutorial will teach you how to do just that: **write a crowdfunding smart contract, deploy it, and use it**. The idea is simple: the smart contract will accept transfers until a deadline is reached, tracking all contributors. -If the deadline is reached and the smart contract has gathered an amount of EGLD above the desired funds, then the smart contract will consider the crowdfunding a success and it will consequently send all the EGLD to a predetermined account (yours!). +If the deadline is reached and the smart contract has gathered an amount of EGLD above the desired funds, then the smart contract will consider the crowdfunding a success, and it will consequently send all the EGLD to a predetermined account (yours!). -However, if the donations fall short of the target, the contract will return all the EGLD to the donors. +However, if the donations fall short of the target, the contract will return all the EGLDs to the donors. [comment]: # (mx-context-auto) ## Design -Here is how the smart contract methods is designed: +Here is how the smart contract methods are designed: - `init`: automatically triggered when the contract is deployed. It takes two inputs from you: - 1. the target amount of EGLD you want to raise - 2. the crowdfunding deadline, which is expressed as a block nonce. -- `fund`: used by donors to contribute EGLD to the campaign. It will receive EGLD and save the necessary details so the contract can return funds if the campaign doesn't reach its goal. + 1. The target amount of EGLD you want to raise; + 2. The crowdfunding deadline, which is expressed as a block nonce. +- `fund`: used by donors to contribute EGLD to the campaign. It will receive EGLD and save the necessary details so the contract can return funds if the campaign doesn't reach its goal; - `claim`: if called before the deadline, it does nothing and returns an error. If called after the deadline: - - By you (the campaign creator), it sends all the raised EGLD to your account if the target amount is met. Otherwise, it returns an error. - - By a donor, it refunds their contribution if the target amount isn’t reached. If the target is met, it does nothing and returns an error. + - By you (the campaign creator), it sends all the raised EGLDs to your account if the target amount is met. Otherwise, it returns an error; + - By donor, it refunds their contribution if the target amount is not reached. If the target is met, it does nothing and returns an error; - By anyone else, it does nothing and returns an error. - `status`: Provides information about the campaign, such as whether it is still active or completed and how much EGLD has been raised so far. You will likely use this frequently to monitor progress. -This tutorial will firstly focus on the `init` method, to get you acquainted with the development process and tools. You will implement `init` and also _write unit tests_ for it. -In this part of the tutorial, we will start with the `init` method to familiarize you with the development process and tools. You wll not only implement the init method but also **create unit tests** to ensure it works as expected. +In this part of the tutorial, we will start with the `init` method to familiarize you with the development process and tools. You will not only implement the init method but also **tests** to ensure it works as expected. -:::note testing +:::important testing Automated testing is exceptionally important for the development of smart contracts, due to the sensitive nature of the information they must handle. ::: [comment]: # (mx-context-auto) -## Prerequisites TODO! +## Prerequisites -[comment]: # (mx-context-auto) - -### sc-meta TODO - -### Rust - -Install **Rust** and [**sc-meta**](/developers/meta/sc-meta) as depicted [here](/sdk-and-tools/troubleshooting/rust-setup). - -[comment]: # (mx-context-auto) - -### VSCode +:::important +Before starting this tutorial, make sure you have the following: +- `stable` **Rust** version `≥ 1.78.0` (install via [rustup](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) +- `sc-meta` (install [multiversx-sc-meta](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) +::: For contract developers, we generally recommend [**VSCode**](https://code.visualstudio.com) with the following extensions: - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) @@ -69,17 +62,15 @@ For contract developers, we generally recommend [**VSCode**](https://code.visual ## Step 1: prepare the workspace -The source code of each smart contract requires its own folder. We will start the development of the **crowdfunding** contract from the **Empty** template. To get the development environment ready, simply run the following commands in your terminal: +The source code of each smart contract requires its own folder. We will start the development of the **crowdfunding** contract from the **empty** template. To get the development environment ready, simply run the following commands in your terminal: ```bash -mkdir -p ~/MultiversX/SmartContracts -cd ~/MultiversX/SmartContracts sc-meta new --name crowdfunding --template empty ``` -You may choose any location you want for your smart contract. The above is just an example. Either way, now that you are in the folder dedicated to the smart contract, we can begin. +You may choose any location you want for your smart contract. Either way, now that you are in the `crowdfunding` folder we can begin. -`sc-meta` created your project out of a template. These templates are contracts written and tested by MultiversX, which can be used by anybody as starting points. +[sc-meta](/docs/developers/meta/sc-meta.md) created your project out of a template. These templates are contracts written and tested by MultiversX, which can be used by anybody as starting points. ```toml title=Cargo.toml [package] @@ -93,13 +84,13 @@ authors = ["you"] path = "src/crowdfunding.rs" [dependencies.multiversx-sc] -version = "0.54.6" +version = "0.55.0" [dev-dependencies] num-bigint = "0.4" [dev-dependencies.multiversx-sc-scenario] -version = "0.54.6" +version = "0.55.0" [workspace] members = [ @@ -108,19 +99,19 @@ members = [ ] ``` -Let's inspect the file found at path `~/MultiversX/SmartContracts/crowdfunding/Cargo.toml`: -- The `[package]` represent the **project** which is unsurprisingly named `crowdfunding`, and has the version `0.0.0`. You can set any version you like, just make sure it has 3 numbers separated by dots. It is a requirement. The `publish` is set to **false** to prevent the package from being published to Rust’s central package registry, crates.io. It's useful for private or experimental projects. -- `[lib]` declares the source code of the smart contracts, which in our case is `src/crowdfunding.rs`. You can name this file anything you want. The default Rust naming is `lib.rs`, but it can be easier organizing your code when the main code files bear the names of the contracts. +Let's inspect the file found at path `crowdfunding/Cargo.toml`: +- `[package]` represents the **project** which is unsurprisingly named `crowdfunding`, and has version `0.0.0`. You can set any version you like, just make sure it has 3 numbers separated by dots. It is a requirement. The `publish` is set to **false** to prevent the package from being published to Rust’s central package registry. It's useful for private or experimental projects; +- `[lib]` declares the source code of the smart contracts, which in our case is `src/crowdfunding.rs`. You can name this file anything you want. The default Rust naming is `lib.rs`, but it can be easier to organise your code when the main code files bear the names of the contracts; - This project has `dependencies` and `dev-dependencies`. You'll need a few special and very helpful packages: - - `multiversx-sc`: developed by MultiversX, it is the interface that the smart contract sees and can use. - - `multiversx-sc-scenario`: developed by MultiversX, it is the interface that defines and runs blockchain scenarios involving smart contracts. + - `multiversx-sc`: developed by MultiversX, it is the interface that the smart contract sees and can use; + - `multiversx-sc-scenario`: developed by MultiversX, it is the interface that defines and runs blockchain scenarios involving smart contracts; - `num-bigint`: for working with arbitrarily large integers. -- `[workspace]` is a group of related Rust projects that share common dependencies or build settings. +- `[workspace]` is a group of related Rust projects that share common dependencies or build settings; - The resulting binary will be the name of the project, which in our case is `crowdfunding` (actually, `crowdfunding.wasm`, but the compiler will add the `.wasm` part). [comment]: # (mx-context-auto) -## Step 2: write the code +## Step 2: develop With the structure in place, you can now write the code and build it. @@ -129,8 +120,8 @@ Open `src/crowdfunding.rs`: ```rust #![no_std] // [1] -#[allow(unused_imports)] // [2] -use multiversx_sc::imports::*; // [3] +use multiversx_sc::imports::*; // [2] +#[allow(unused_imports)] // [3] /// An empty contract. To be used as a template when starting a new contract from scratch. #[multiversx_sc::contract] // [4] @@ -146,12 +137,12 @@ pub trait Crowdfunding { // [5] Let's take a look at the code: - **[1]**: means that the smart contract **has no access to standard libraries**. That will make the code lean and very light. -- **[2]**: brings imports module from the the multiversx_sc crate into **crowdfunding** contract. It effectively grants you access to the [MultiversX framework for Rust smart contracts](https://github.com/multiversx/mx-sdk-rs), which is designed to simplify the code **enormously**. -- **[3]**: since the contract is still in an early stage of development, clippy (Rust's linter) will flag some imports as unused. For now, we will ignore this kind of errors. +- **[2]**: brings imports module from the [multiversx_sc](https://crates.io/crates/multiversx-sc) crate into **Crowdfunding** contract. It effectively grants you access to the [MultiversX framework for Rust smart contracts](https://github.com/multiversx/mx-sdk-rs), which is designed to simplify the code **enormously**. +- **[3]**: since the contract is still in an early stage of development, clippy (Rust's linter) will flag some imports as unused. For now, we will ignore this kind of error. - **[4]**: processes the **Crowdfunding** trait definition as a **smart contract** that can be deployed on the MultiversX blockchain. - **[5]**: the contract [trait](https://doc.rust-lang.org/book/ch10-02-traits.html) where all the endpoints will be developed. - **[6]**: marks the following method (`init`) as the constructor function for the contract. -- **[7]**: this is the constructor itself. It receives the contract's instance as parameter (_&self_). The method is called once the contract is deployed on the MultiversX blockchain. You can name it any way you wish, but it must be annotated with `#[init]`. For the moment, no initialization logic is defined. +- **[7]**: this is the constructor itself. It receives the contract's instance as a parameter (_&self_). The method is called once the contract is deployed on the MultiversX blockchain. You can name it any way you wish, but it must be annotated with `#[init]`. For the moment, no initialization logic is defined. - **[8]**: marks the following method (`upgrade`) as the upgrade function for the contract. It is called when the contract is re-deployed to the same address. - **[9]**: this is the upgrade method itself. Similar to [7], it takes a reference to the contract instance (_&self_) and performs no specific logic here. @@ -160,7 +151,7 @@ Let's take a look at the code: ## Step 3: build -Now go back to the terminal, make sure the current folder is the one containing the Crowdfunding smart contract (`~/MultiversX/SmartContracts/crowdfunding`), then issue the **build** command: +Now go back to the terminal, make sure the current folder is the one containing the Crowdfunding smart contract (`crowdfunding/`), then trigger the **build** command: ```bash sc-meta all build @@ -168,106 +159,115 @@ sc-meta all build If this is the first time you build a Rust smart contract with the `sc-meta` command, it will take a little while before it's done. Subsequent builds will be much faster. -When the command completes, a new folder will appear: `/output`. This folder contains: +When the command completes, a new folder will appear: `crowdfunding/output/`. This folder contains: 1. `crowdfunding.abi.json` 2. `crowdfunding.imports.json` 3. `crowdfunding.mxsc.json` 4. `crowdfunding.wasm` -We won't be doing anything with these files just yet - wait until we get to the deployment part. Along with `/output`, there are a few other folders and files generated. You can safely ignore them for now, but do not delete the `/wasm` folder - it's what makes the build command faster after the initial run. +We won't be doing anything with these files just yet - wait until we get to the deployment part. Along with `crowdfunding/output/`, there are a few other folders and files generated. You can safely ignore them for now, but do not delete the `/crowdfunding/wasm/` folder - it's what makes the build command faster after the initial run. -`/tests` can be safely deleted, as they are not important for this contract: +The following can be safely deleted, as they are not important for this contract: +- The `scenarios/` folder; +- The `crowdfunding/tests/crowdfunding_scenario_go_test.rs` file; +- The `crowdfunding/tests/crowdfunding_scenario_rs_test.rs` file. -The structure of your folder should be like this (output printed using command `tree -L 3`): +The structure of your folder should be like this (output printed using command `tree -L 2`): ```bash . +├── Cargo.lock ├── Cargo.toml ├── meta │ ├── Cargo.toml │ └── src -│ └── main.rs +├── multiversx.json ├── output │ ├── crowdfunding.abi.json +│ ├── crowdfunding.imports.json +│ ├── crowdfunding.mxsc.json │ └── crowdfunding.wasm -├── scenarios -│ └── crowdfunding.scen.json ├── src │ └── crowdfunding.rs +├── target +│ ├── CACHEDIR.TAG +│ ├── debug +│ ├── release +│ ├── tmp +│ └── wasm32-unknown-unknown +├── tests └── wasm + ├── Cargo.lock ├── Cargo.toml └── src - └── lib.rs ``` It's time to add some functionality to the `init` function now. [comment]: # (mx-context-auto) -## Step 4: extend init +## Step 4: persisting values -In this step you will use the `init` method to persist some values in the storage of the Crowdfunding smart contract. - +In this step, you will use the `init` method to persist some values in the storage of the Crowdfunding smart contract. [comment]: # (mx-context-auto) ### Storage mappers -Every smart contract is allowed to store key-value pairs into a persistent structure, created for the smart contract at the moment of its deployment on the MultiversX Network. +Every smart contract can store key-value pairs in a persistent structure, created for the smart contract at its deployment on the MultiversX Network. -The storage of a smart contract is, for all intents and purposes, a generic hash map or dictionary. When you want to store some arbitrary value, you store it under a specific key. To get the value back, you need to know the key you stored it under. +The storage of a smart contract is, for all intents and purposes, **a generic hash map or dictionary**. When you want to store some arbitrary value, you store it under a specific key. To get the value back, you need to know the key you stored it under. -To help you with keeping the code clean, the framework enables you to write setter and getter methods for individual key-value pairs. There are several ways to interact with storage from a contract, but the simplest one is by using storage mappers. Here is a simple mapper, dedicated to storing / retrieving the value stored under the key `target`: +To help you keep the code clean, the framework enables you to write **setter** and **getter** methods for individual key-value pairs. There are several ways to interact with storage from a contract, but the simplest one is by using [**storage mappers**](/docs/developers/developer-reference/storage-mappers.md). + +Next, you will declare a [_SingleValueMapper_](/docs/developers/developer-reference/storage-mappers.md#singlevaluemapper) that has the purpose of storing a [_BigUint_](/docs/developers/best-practices/biguint-operations.md) number. This storage mapper is dedicated to storing/retrieving the value stored under the key `target`: ```rust #[storage_mapper("target")] fn target(&self) -> SingleValueMapper; ``` -The methods above treat the stored value as having a specific **type**, namely the type `BigUint`. Under the hood, `BigUint` is a big unsigned number, handled by the VM. There is no need to import any library, big number arithmetic is provided for all contracts out of the box. +:::important +`BigUint` **type** is a big unsigned number, handled by the VM. There is no need to import any library, big number arithmetic is provided for all contracts out of the box. +::: -Normally, smart contract developers are used to dealing with raw bytes when storing or loading values from storage. The MultiversX framework for Rust smart contracts makes it far easier to manage the storage, because it can handle typed values automatically. +Normally, smart contract developers are used to dealing with raw bytes when storing or loading values from storage. The MultiversX framework for Rust smart contracts makes it far easier to manage the storage because it can handle typed values automatically. [comment]: # (mx-context-auto) -### **Setting some targets** +### Extend init -You will now instruct the `init` method to store the amount of tokens that should be gathered, upon deployment. +You will now instruct the `init` method to store the amount of tokens that should be gathered upon deployment. -The owner of a smart contract is the account which deployed it (you). By design, your Crowdfunding smart contract will send all the donated EGLD to its owner (you), assuming the target amount was reached. Nobody else has this privilege, because there is only one single owner of any given smart contract. +The owner of a smart contract is the account that deployed it (you). By design, your Crowdfunding smart contract will send all the donated EGLD to its owner (you), assuming the target amount was reached. Nobody else has this privilege, because there is only one single owner of any given smart contract. -Here's how the `init` method looks like, with the code that saves the target (guess who): +Here's how the `init` method looks, with the code that saves the target: ```Rust -#![no_std] - -multiversx_sc::imports!(); - -#[multiversx_sc::contract] -pub trait Crowdfunding { - - #[storage_mapper("target")] - fn target(&self) -> SingleValueMapper; - - #[init] - fn init(&self, target: BigUint) { - self.target().set(&target); - } +#[init] +fn init(&self, target: BigUint) { + self.target().set(&target); } ``` -We have added an argument to the constructor method, the argument is called `target` and will need to be supplied when we deploy the contract. The argument then proptly gets saved to storage. +We have added an argument to the constructor method. It is called `target` and will need to be supplied when we deploy the contract. The argument then promptly gets saved to storage. -Now note the `self.target()` invocation. This gives us an object that acts like a proxy to a part of the storage. Calling the `.set()` method on it causes the value to be saved to the contract storage. +Now note the `self.target()` invocation. This gives us an object that acts like a proxy for a part of the storage. Calling the `.set()` method on it causes the value to be saved to the contract storage. -Well, not quite. All of the stored values only actually end up in the storage if the transaction completes successfully. Smart contracts cannot access the protocol directly, it is the VM that intermediates everything. +:::important +All of the stored values end up in the storage if the transaction completes successfully. Smart contracts cannot access the protocol directly, it is the VM that intermediates everything. +::: Whenever you want to make sure your code is in order, run the build command: ```bash sc-meta all build ``` -There's one more thing: by default, none of the `fn` statements declare smart contract methods which are _externally callable_. All the data in the contract is publicly available, but it can be cumbersome to search through the contract storage manually. That is why it is often nice to make getters public, so people can call them to get specific data out. Public methods are annotated with either `#[endpoint]` or `#[view]`. There is currently no difference in functionality between them (but there might be at some point in the future). Semantically, `#[view]` indicates readonly methods, while `#[endpoint]` suggests that the method also changes the contract state. You can also think of `#[init]` as a special type of endpoint. + +There's one more thing: by default, none of the `fn` statements declare smart contract methods that are _externally callable_. All the data in the contract is publicly available, but it can be cumbersome to search through the contract storage manually. That is why it is often nice to make getters public, so people can call them to get specific data out. + +Public methods are annotated with either `#[endpoint]` or `#[view]`. There is currently no difference in functionality between them (but there might be at some point in the future). Semantically, `#[view]` indicates readonly methods, while `#[endpoint]` suggests that the method also changes the contract state. + ```rust #[view] @@ -275,260 +275,280 @@ There's one more thing: by default, none of the `fn` statements declare smart co fn target(&self) -> SingleValueMapper; ``` +You can also think of `#[init]` as a special type of endpoint. + [comment]: # (mx-context-auto) -### **But will you remember?** +## Step 5: testing + +You must always make sure that the code you write functions as intended. That's what **automatic testing** is for. + +For now, this is how your contract looks: +```rust +#![no_std] + +#[allow(unused_imports)] +use multiversx_sc::imports::*; + +#[multiversx_sc::contract] +pub trait Crowdfunding { + #[init] + fn init(&self, target: BigUint) { + self.target().set(&target); + } + + #[upgrade] + fn upgrade(&self) {} + + #[view] + #[storage_mapper("target")] + fn target(&self) -> SingleValueMapper; +} +``` + +There are several ways to write [smart contract tests in Rust](/docs/developers/testing/rust/sc-test-overview.md). Now, we will focus on developing a test using [black-box calls](/docs/developers/testing/rust/sc-blackbox-calls.md). -You must always make sure that the code you write functions as intended. That's what automatic testing is for. +:::important +Blackbox tests execution imitates the blockchain with no access to private contract functions. +::: -Let's write a test against the `init` method, and make sure that it definitely stores the address of the owner under the `target` key, at deployment. +Let's write a test against the `init` method to make sure that it definitely stores the address of the owner under the `target` key at deployment. -To test `init`, you will write a JSON file which describes what to do with the smart contract and what is the expected output. In the folder of the Crowdfunding smart contract, there is a folder called `scenarios`. Inside it, there is a file called `crowdfunding.scen.json`. Rename that file to`crowdfunding-init.scen.json` ( `scen` is short for "scenario"). +[comment]: # (mx-context-auto) -Your folder should look like this (output from the command `tree -L 3`): +### Set up -```text +In the folder of the Crowdfunding smart contract, there is a folder called `tests/`. Inside it, create a new Rust file called `crowdfunding_blackbox_test.rs`. + +Your folder should look like this (output from the command `tree -L 2`): + +```bash . +├── Cargo.lock ├── Cargo.toml ├── meta │ ├── Cargo.toml │ └── src -│ └── main.rs +├── multiversx.json ├── output │ ├── crowdfunding.abi.json +│ ├── crowdfunding.imports.json +│ ├── crowdfunding.mxsc.json │ └── crowdfunding.wasm -├── scenarios -│ └── crowdfunding-init.scen.json ├── src -│ └── lib.rs +│ └── crowdfunding.rs +├── target +│ ├── CACHEDIR.TAG +│ ├── debug +│ ├── release +│ ├── tmp +│ └── wasm32-unknown-unknown +├── tests +│ └── crowdfunding_blackbox_test.rs └── wasm ├── Cargo.lock ├── Cargo.toml - ├── src - └── target -``` -Let's define the first test scenario. Open the file `scenarios/crowdfunding-init.scen.json` in your favorite text editor and replace its contents with the following code. It might look like a lot, but we'll go over every bit of it, and it's not really that complicated. - -```json -{ - "name": "crowdfunding deployment test", - "steps": [ - { - "step": "setState", - "accounts": { - "address:my_address": { - "nonce": "0", - "balance": "1,000,000" - } - }, - "newAddresses": [ - { - "creatorAddress": "address:my_address", - "creatorNonce": "0", - "newAddress": "sc:crowdfunding" - } - ] - }, - { - "step": "scDeploy", - "txId": "deploy", - "tx": { - "from": "address:my_address", - "contractCode": "file:../output/crowdfunding.wasm", - "arguments": ["500,000,000,000"], - "gasLimit": "5,000,000", - "gasPrice": "0" - }, - "expect": { - "out": [], - "status": "0", - "gas": "*", - "refund": "*" - } - }, - { - "step": "checkState", - "accounts": { - "address:my_address": { - "nonce": "1", - "balance": "1,000,000", - "storage": {} - }, - "sc:crowdfunding": { - "nonce": "0", - "balance": "0", - "storage": { - "str:target": "500,000,000,000" - }, - "code": "file:../output/crowdfunding.wasm" - } - } - } - ] -} + └── src ``` -Save the file. Do you want to try it out first? Go ahead and issue this command on your terminal: +Before creating the first test, we need to [set up the environment](/docs/developers/testing/rust/sc-test-setup.md). We will: +1. Generate the smart contract's proxy; +2. Register the contract; +3. Set up accounts. -```bash -cargo test -``` +[comment]: # (mx-context-auto) -If everything went well, you should see an all-capitals, loud `SUCCESS` being printed, like this: +### Generate Proxy -```rust -Scenario: crowdfunding-init.scen.json ... ok -Done. Passed: 1. Failed: 0. Skipped: 0. -SUCCESS +A smart contract's [proxy](/docs/developers/transactions/tx-proxies.md) is an object that mimics the contract. We will use the proxy to call the endpoints of the Crowdfunding contracts. + +The proxy contains entirely autogenerated code. However, before running the command to generate the proxy, we need to set up a configuration file. + +In the root of the contract, at the path `crowdfunding/`, we will create the configuration file `sc-config.toml`, where we will specify the path to generate the proxy: + +```toml title=sc-config.toml +[settings] + +[[proxy]] +path = "src/crowdfunding_proxy.rs" ``` -You need to understand the contents of this JSON file - again, the importance of testing your smart contracts cannot be overstated. +In the terminal, in the root of the contract, we will run the next command that will generate the proxy for the Crowdfunding smart contract: -[comment]: # (mx-context-auto) +```bash +sc-meta all proxy +``` -### **So what just happened?** +Once the proxy is generated, our work is not over yet. The next thing to do is to import the module in the Crowdfunding smart contract's code: -You ran a testing command which interpreted a JSON scenario. Line number 2 contains the name of this scenario, namely `crowdfunding deployment test`. This test was executed in an isolated environment, which contains the MultiversX WASM VM and a simulated blockchain. It's as close to the real MultiversX Network as you can get — save from running your own local testnet, of course, but you don't need to think about that right now. +```rust +#![no_std] -A scenario has steps, which will be executed in the sequence they appear in the JSON file. Observe on line 3 that the field `steps` is a JSON list, containing three scenario steps. +#[allow(unused_imports)] +use multiversx_sc::imports::*; -Looking at the JSON file, you may be tempted to assume that the meaning of `"step": "setState"` is simply to give a name to the scenario step. That is incorrect, because `"step": "setState"` means that the _type_ of this step is `setState`, i.e. to prepare the state of the testing environment for the following scenario steps. +pub mod crowdfunding_proxy; -The same goes for `"step": "scDeploy"`, which is a scenario step that performs the deployment of a SmartContract. As you probably guessed, the last scenario step has the type `checkState`: it describes your expectations about the testing environment, after running the previous scenario steps. +#[multiversx_sc::contract] +pub trait Crowdfunding { + // Here is the implementation of the crowdfunding contract +} +``` -The following subsections will discuss each of the steps individually. +With each build of the contract executed by the developer, the proxy will be automatically updated with the changes made to the contract. [comment]: # (mx-context-auto) -## **Scenario step "setState"** +### Register -[comment]: # (mx-context-auto) +The Rust backend does not run compiled contracts, instead, it hooks the actual Rust contract code to its engine. You can find more [here](/docs/developers/testing/rust/sc-test-setup.md#registering-contracts). -### **You're you, but in a different universe** +In order to link the smart contract code to the test you are developing, you need to call `register_contract()` in the setup function of the blackbox test. -The first scenario step begins by declaring the accounts that exist in the fictional universe in which the Crowdfunding smart contract will be tested. +```rust +use crowdfunding::crowdfunding_proxy; +use multiversx_sc_scenario::imports::*; -There is only one account defined - the one that will perform the deployment during the test. The smart contract will believe that it is owned by this account. In the JSON file, you wrote: +const CODE_PATH: MxscPath = MxscPath::new("output/crowdfunding.mxsc.json"); -```json -"accounts": { - "address:my_address": { - "nonce": "0", - "balance": "1,000,000" - } -}, +fn world() -> ScenarioWorld { + let mut blockchain = ScenarioWorld::new(); + blockchain.set_current_dir_from_workspace("crowdfunding"); + blockchain.register_contract(CODE_PATH, crowdfunding::ContractBuilder); + blockchain +} ``` -This defines the account with the address `my_address`, which the testing environment will use to pretend it's you. Note that in this fictional universe, your account nonce is `0` (meaning you've never used this account yet) and your `balance` is `1,000,000`. Note: EGLD has 18 decimals, so 1 EGLD would be equal to `1,000,000,000,000,000,000` (10^18), but you rarely need to work with such big values in tests. +### Account -Note that there are is the text `address:`at the beginning of `my_address`, which instructs the testing environment to treat the immediately following string as a 32-byte address (by also adding the necessary padding to reach the required length), i.e. it shouldn't try to decode it as a hexadecimal number or anything else. All addresses in the JSON file above are defined with leading `address:`, and all smart contracts with `sc:`. +The environment you're working in is a mocked blockchain. This means you have to create and manage accounts, allowing you to test and verify the behavior of your functions without deploying to a real blockchain. -[comment]: # (mx-context-auto) +Here's an example to get started in `crowdfunding_blackbox_test.rs`: -### **Imaginary address generator** +```rust +const OWNER: TestAddress = TestAddress::new("owner"); -Immediately after the `accounts`, the first scenario step contains the following block: +#[test] +fn crowdfunding_deploy_test() { + let mut world = world(); -```json -"newAddresses": [ - { - "creatorAddress": "address:my_address", - "creatorNonce": "0", - "newAddress": "sc:crowdfunding" - } -] + world.account(OWNER).nonce(0).balance(1000000); +} ``` -In short, this block instructs the testing environment to pretend that the address to be generated for the first (nonce `0`) deployment attempted by `my_address` must be the address`crowdfunding`. - -Makes sense, doesn't it? If you didn't write this, the testing environment would have deployed the Crowdfunding smart contract at some auto-generated address, which we wouldn't be informed of, so we couldn't interact with the smart contract in the subsequent scenario steps. - -But with the configured `newAddresses` generator, we know that every run of the test will deploy the smart contract at the address `the_crowdfunding_contract`. +In the snippet above, we've added only one account to the fictional universe of Crowdfunding smart contract. It is an account with the address `owner`, which the testing environment will use to pretend it's you. Note that in this fictional universe, your account nonce is `0` (meaning you've never used this account yet) and your `balance` is `1,000,000`. -While it's not important to know right now, the `newAddresses` generator can be configured to produce fixed addresses for multiple smart contract deployments and even for multiple addresses that perform the deployment! - -[comment]: # (mx-context-auto) +:::important +No transaction can start if that account does not exist in the mocked blockchain. More explanations can be found [here](/docs/developers/testing/rust/sc-test-setup.md#setting-accounts). +::: -## **Scenario step "scDeploy"** +### Deploy -The next scenario step defined by the JSON file instructs the testing environment to perform the deployment itself. Observe: +The purpose of the account created one step ago is to act as the owner of the Crowdfunding smart contract. To make this happen, the **OWNER** constant will serve as the transaction **sender**. -```json -"tx": { - "from": "address:my_address", - "contractCode": "file:../output/crowdfunding.wasm", - "arguments": [ "500,000,000,000" ], - "value": "0", - "gasLimit": "1,000,000", - "gasPrice": "0" -}, +```rust +const CROWDFUNDING_ADDRESS: TestSCAddress = TestSCAddress::new("crowdfunding"); + +#[test] +fn crowdfunding_deploy_test() { + /* + Set up account + */ + + let crowdfunding_address = world + .tx() + .from(OWNER) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .init(500_000_000_000u64) + .code(CODE_PATH) + .new_address(CROWDFUNDING_ADDRESS) + .returns(ReturnsNewAddress) + .run(); +} ``` -This describes a deployment transaction. It was fictionally submitted by "you", using your account with the address `my_address`. +The transaction above is a deploy call that stores in `target` value `500,000,000,000`. It was fictionally submitted by "you", using your account with the address `owner`. -This deployment transaction contains the WASM bytecode of the Crowdfunding smart contract, which is read at runtime from the file `output/crowdfunding.wasm`. +`.new_address(CROWDFUNDING_ADDRESS)` marks that the address of the deployed contracts will be the value stored in the **CROWDFUNDING_ADDRESS** constant. -Remember to run `sc-meta all build` before running the test, especially if you made recent changes to the smart contract source code! The WASM bytecode will be read directly from the file you specify here, without rebuilding it automatically. +`.code(CODE_PATH)` explicitly sets the deployment Crowdfunding's code source as bytes. -"You" also sent exactly `value: 0` EGLD out of the `1,000,000` to the deployed smart contract. It wouldn't need them anyway, because your Crowdfunding smart contract won't be transferring any EGLD to anyone, unless they donate it first. +:::note +Deploy calls are specified by the code source. You can find more details about what data needs a transaction [here](/docs/developers/transactions/tx-data.md). +::: -The fields `gasLimit` and `gasPrice` shouldn't concern you too much. It's important that `gasLimit` needs to be high, and `gasPrice` may be 0. Just so you know, the real MultiversX Network would calculate the transaction fee from these values. On the real MultiversX Network, you cannot set a `gasPrice` of 0, for obvious reasons. +:::important +Remember to run `sc-meta all build` before running the test, especially if you made recent changes to the smart contract source code! Code source will be read directly from the file you specify through the **MxscPath** constant, without rebuilding it automatically. +::: -[comment]: # (mx-context-auto) +### Checks -### **The result of the deployment** +What's the purpose of testing if we do not validate the behavior of the entities interacting with the blockchain? Let's take the next step by enhancing the `crowdfunding_deploy_test()` function to include verification operations. -Once the testing environment executes the deployment transaction described above, you have the opportunity to assert its successful completion: +Once the deployment is executed, we will verify if: +- The **contract address** is **CROWDFUNDING_ADDRESS**; +- The **owner** has no less EGLD than the value with which it was initialized: `1,000,000`; +- `target` contains the value set at deployment: `500,000,000,000`. -```json -"expect": { - "out": [], - "status": "0", - "gas": "*", - "refund": "*" +```rust +#[test] +fn crowdfunding_deploy_test() { + /* + Set up account + Deploy + */ + + assert_eq!(crowdfunding_address, CROWDFUNDING_ADDRESS.to_address()); + + world.check_account(OWNER).balance(1_000_000); + + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .target() + .returns(ExpectValue(500_000_000_000u64)) + .run(); } ``` +Notice that there are two accounts now, not just one. There's evidently the account `owner` and the new account `crowdfunding`, as a result of the deployment transaction. -The only important field here is `"status": "0"`, which is the actual return code coming from the MultiversX VM after it executed the deployment transaction. `0` means success, of course. +:::important +Smart contracts _are_ accounts in the MultiversX Network, accounts with associated code, which can be executed when transactions are sent to them. +::: -The `out` array would contain values returned by your smart contract call (in this case, the `init` function doesn't return anything, but it could if the developer wanted). +The **owner's balance** remains unchanged - the deployment transaction did not cost anything, because the gas price is set to `0` in the **testing environment**. -The remaining two fields `gas` and `refund` allow you to specify how much gas you expect the deployment transaction to consume, and how much EGLD you'd receive back as a result of overestimating the `gasLimit`. These are both set to `"*"` here, meaning that we don't care right now about their actual values. +:::note +The `.check_account(OWNER)` method verifies whether an account exists at the specified address and checks its ownership details. Details available [here](/docs/developers/testing/rust/sc-blackbox-example.md#check-accounts). +::: -[comment]: # (mx-context-auto) +:::note +The `.query()` method is used to interact with the smart contract's view functions via the proxy, retrieving information without modifying the blockchain state. Details available [here](/docs/developers/testing/rust/sc-blackbox-calls.md#query). +::: -## **Scenario step "checkState"** - -The final scenario step mirrors the first scenario step. There's an `accounts` field again, but with more content: - -```json -"accounts": { - "address:my_address": { - "nonce": "1", - "balance": "1,000,000" - }, - "sc:crowdfunding": { - "code": "file:../output/crowdfunding.wasm", - "nonce": "0", - "balance": "0", - "storage": { - "str:target": "500,000,000,000" - } - } -} +## Run test + +Do you want to try it out first? Go ahead and issue this command on your terminal at path `/crowdfunding`: + +```bash +cargo test ``` -Notice that there are two accounts now, not just one. There's evidently the account `my_address`, which we knew it existed, after defining it ourselves in the first scenario step. But a new account appeared, `the_crowdfunding_contract`, as a result of the deployment transaction executed in the second scenario step. This is because smart contracts _are_ accounts in the MultiversX Network, accounts with associated code, which can be executed when transactions are sent to them. +If everything went well, you should see the following being printed: -The account `my_address` now has the nonce `1`, because a transaction has been executed, sent from it. Its balance remains unchanged - the deployment transaction did not cost anything, because the `gasPrice` field was set to `0` in the second scenario step. This is only allowed in tests, of course. +```rust +running 1 test +test crowdfunding_deploy_test ... ok -The account `crowdfunding` is the Crowdfunding smart contract. We assert that it contains the bytecode specified by the file `output/crowdfunding.wasm` (path relative to the JSON file). We also assert that its `nonce` is `0`, which means that the contract itself has never deployed a "child" contract of its own (which is technically possible). The `balance` of the smart contract account is `0`, because it didn't receive any EGLD as part of the deployment transaction, nor did we specify any scenario steps that transfer EGLD to it (we'll do that soon). +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.05s +``` -And finally, we assert that the smart contract storage contains `500,000,000,000` under the `target` key, which is what the `init` function was supposed to make sure. The smart contract has, therefore, remembered the target you set for it. +You need to understand the contents of this blackbox test - again, the importance of testing your smart contracts cannot be overstated. [comment]: # (mx-context-auto) -## **Next up** +## Next up -The tutorial will continue with the definition of the `fund`, `claim` and `status` function, and will guide you through writing JSON test scenarios for them. +The tutorial will continue with defining of the `fund`, `claim` and `status` function, and will guide you through writing [blackbox tests](/docs/developers//testing/rust/sc-blackbox-calls.md) for them. From 9e8aa08306253a160b44153b4808f18ea2d11ae7 Mon Sep 17 00:00:00 2001 From: BiancaIalangi Date: Fri, 21 Feb 2025 16:49:26 +0200 Subject: [PATCH 3/9] update crowdfunding p2 + update overview --- docs/developers/overview.md | 13 +- docs/developers/tutorials/crowdfunding-p1.md | 59 +- docs/developers/tutorials/crowdfunding-p2.md | 767 ++++++++++--------- 3 files changed, 454 insertions(+), 385 deletions(-) diff --git a/docs/developers/overview.md b/docs/developers/overview.md index 28c7d62e3..7f549d66c 100644 --- a/docs/developers/overview.md +++ b/docs/developers/overview.md @@ -7,7 +7,8 @@ title: Developers - Overview This page serves as the landing destination for builders seeking to construct on the Multiversx platform. -If anything is missing, or you want to get more support, please refer to Discord or Telegram developers chats: +If anything is missing, or you want to get more support, please refer to Discord or Telegram developers chats: + - [Discord: MultiversX Builders](https://discord.gg/multiversxbuilders) - [Telegram: MultiversX Developers](https://t.me/MultiversXDevelopers) @@ -25,6 +26,7 @@ For interacting with MultiversX Blockchain via SDKs or Rest API, please refer to [comment]: # (mx-context-auto) ## Table of contents + A list with everything that you can explore as a developer on MultiversX. [comment]: # (mx-context-auto) @@ -38,14 +40,14 @@ Below is a list of tutorials for building on MultiversX: | [Build your first dApp in 15 minutes](/developers/tutorials/your-first-dapp) | Video + written tutorial on how to create your first dApp. | | [Cryptozombies Tutorials](https://cryptozombies.io/en/multiversx) | Interactive way of learning how to write MultiversX Smart Contracts. | | [Build a microservice for your dApp](/developers/tutorials/your-first-microservice) | Video + written tutorial on how to create your microservice. | -| [Crowdfunding Smart Contract](/developers/tutorials/crowdfunding-p1) | Crowdfunding tutorial (Part 1). | -| [Crowdfunding Smart Contract](/developers/tutorials/crowdfunding-p2) | Crowdfunding tutorial (Part 2). | +| [Building a Crowdfunding Smart Contract](/docs/developers/tutorials/crowdfunding-p2.md) | Write, build, and test a simple smart contract. | +| [Enhancing the Crowdfunding Smart Contract](/docs/developers/tutorials/crowdfunding-p2.md) | Expand and refine the functionality of an existing contract.| | [Staking contract Tutorial](/developers/tutorials/staking-contract) | Step by step tutorial on how to create a Staking Smart Contract. | | [Energy DAO Tutorial](/developers/tutorials/energy-dao) | In depth analysis of the Energy DAO SC template. | | [DEX Walkthrough](/developers/tutorials/dex-walkthrough) | In depth walkthrough of all the main DEX contracts. | | [WalletConnect 2.0 Migration](/developers/tutorials/wallet-connect-v2-migration) | WalletConnect 2.0 Migration Guide | | [Ethereum to MultiversX migration guide](/developers/tutorials/eth-to-mvx) | Guide for Ethereum developers to start building on MultiversX. | -| [Chain Simulator in Adder - SpaceCraft interactors](/developers/tutorials/chain-simulator-adder)| Guide on how to interact with Chain Simulator in one of the simplest SCs.| +| [Chain Simulator in Adder - SpaceCraft interactors](/developers/tutorials/chain-simulator-adder)| Guide on how to interact with Chain Simulator in one of the simplest SCs.| [comment]: # (mx-context-auto) @@ -123,7 +125,6 @@ Learn about transaction's gas and how a fee is calculated: | [Whitebox framework functions reference](/developers/testing/rust/whitebox-legacy-functions-reference) | A list of available functions to be used when using the whitebox framework. | | [Debugging](/developers/testing/sc-debugging) | How to debug your smart contract tests. | - [comment]: # (mx-context-auto) ### Scenarios Reference @@ -156,7 +157,7 @@ the execution of smart contract, information about ESDT transfers or built-in fu [comment]: # (mx-context-auto) -#### Event logs can be categorized into the following types: +#### Event logs can be categorized into the following types | Name | Description | | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | diff --git a/docs/developers/tutorials/crowdfunding-p1.md b/docs/developers/tutorials/crowdfunding-p1.md index d4d6c1df1..87c8b43b0 100644 --- a/docs/developers/tutorials/crowdfunding-p1.md +++ b/docs/developers/tutorials/crowdfunding-p1.md @@ -1,6 +1,6 @@ --- id: crowdfunding-p1 -title: The Crowdfunding Smart Contract +title: Building a Crowdfunding Smart Contract --- [comment]: # (mx-abstract) Write, build and deploy a simple smart contract written in Rust. @@ -28,7 +28,8 @@ However, if the donations fall short of the target, the contract will return all ## Design Here is how the smart contract methods are designed: -- `init`: automatically triggered when the contract is deployed. It takes two inputs from you: + +- `init`: automatically triggered when the contract is deployed. It takes two inputs from you: 1. The target amount of EGLD you want to raise; 2. The crowdfunding deadline, which is expressed as a block nonce. - `fund`: used by donors to contribute EGLD to the campaign. It will receive EGLD and save the necessary details so the contract can return funds if the campaign doesn't reach its goal; @@ -46,17 +47,20 @@ Automated testing is exceptionally important for the development of smart contra [comment]: # (mx-context-auto) -## Prerequisites +## Prerequisites :::important Before starting this tutorial, make sure you have the following: + - `stable` **Rust** version `≥ 1.78.0` (install via [rustup](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) - `sc-meta` (install [multiversx-sc-meta](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) + ::: For contract developers, we generally recommend [**VSCode**](https://code.visualstudio.com) with the following extensions: - - [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) - - [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) + +- [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) +- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb) [comment]: # (mx-context-auto) @@ -84,13 +88,13 @@ authors = ["you"] path = "src/crowdfunding.rs" [dependencies.multiversx-sc] -version = "0.55.0" +version = "0.56.1" [dev-dependencies] num-bigint = "0.4" [dev-dependencies.multiversx-sc-scenario] -version = "0.55.0" +version = "0.56.1" [workspace] members = [ @@ -100,8 +104,9 @@ members = [ ``` Let's inspect the file found at path `crowdfunding/Cargo.toml`: + - `[package]` represents the **project** which is unsurprisingly named `crowdfunding`, and has version `0.0.0`. You can set any version you like, just make sure it has 3 numbers separated by dots. It is a requirement. The `publish` is set to **false** to prevent the package from being published to Rust’s central package registry. It's useful for private or experimental projects; -- `[lib]` declares the source code of the smart contracts, which in our case is `src/crowdfunding.rs`. You can name this file anything you want. The default Rust naming is `lib.rs`, but it can be easier to organise your code when the main code files bear the names of the contracts; +- `[lib]` declares the source code of the smart contracts, which in our case is `src/crowdfunding.rs`. You can name this file anything you want. The default Rust naming is `lib.rs`, but it can be easier to organize your code when the main code files bear the names of the contracts; - This project has `dependencies` and `dev-dependencies`. You'll need a few special and very helpful packages: - `multiversx-sc`: developed by MultiversX, it is the interface that the smart contract sees and can use; - `multiversx-sc-scenario`: developed by MultiversX, it is the interface that defines and runs blockchain scenarios involving smart contracts; @@ -113,7 +118,7 @@ Let's inspect the file found at path `crowdfunding/Cargo.toml`: ## Step 2: develop -With the structure in place, you can now write the code and build it. +With the structure in place, you can now write the code and build it. Open `src/crowdfunding.rs`: @@ -137,16 +142,15 @@ pub trait Crowdfunding { // [5] Let's take a look at the code: - **[1]**: means that the smart contract **has no access to standard libraries**. That will make the code lean and very light. -- **[2]**: brings imports module from the [multiversx_sc](https://crates.io/crates/multiversx-sc) crate into **Crowdfunding** contract. It effectively grants you access to the [MultiversX framework for Rust smart contracts](https://github.com/multiversx/mx-sdk-rs), which is designed to simplify the code **enormously**. +- **[2]**: brings imports module from the [multiversx_sc](https://crates.io/crates/multiversx-sc) crate into **Crowdfunding** contract. It effectively grants you access to the [MultiversX framework for Rust smart contracts](https://github.com/multiversx/mx-sdk-rs), which is designed to simplify the code **enormously**. - **[3]**: since the contract is still in an early stage of development, clippy (Rust's linter) will flag some imports as unused. For now, we will ignore this kind of error. - **[4]**: processes the **Crowdfunding** trait definition as a **smart contract** that can be deployed on the MultiversX blockchain. - **[5]**: the contract [trait](https://doc.rust-lang.org/book/ch10-02-traits.html) where all the endpoints will be developed. - **[6]**: marks the following method (`init`) as the constructor function for the contract. -- **[7]**: this is the constructor itself. It receives the contract's instance as a parameter (_&self_). The method is called once the contract is deployed on the MultiversX blockchain. You can name it any way you wish, but it must be annotated with `#[init]`. For the moment, no initialization logic is defined. +- **[7]**: this is the constructor itself. It receives the contract's instance as a parameter (_&self_). The method is called once the contract is deployed on the MultiversX blockchain. You can name it any way you wish, but it must be annotated with `#[init]`. For the moment, no initialization logic is defined. - **[8]**: marks the following method (`upgrade`) as the upgrade function for the contract. It is called when the contract is re-deployed to the same address. - **[9]**: this is the upgrade method itself. Similar to [7], it takes a reference to the contract instance (_&self_) and performs no specific logic here. - [comment]: # (mx-context-auto) ## Step 3: build @@ -160,7 +164,8 @@ sc-meta all build If this is the first time you build a Rust smart contract with the `sc-meta` command, it will take a little while before it's done. Subsequent builds will be much faster. When the command completes, a new folder will appear: `crowdfunding/output/`. This folder contains: -1. `crowdfunding.abi.json` + +1. `crowdfunding.abi.json` 2. `crowdfunding.imports.json` 3. `crowdfunding.mxsc.json` 4. `crowdfunding.wasm` @@ -168,6 +173,7 @@ When the command completes, a new folder will appear: `crowdfunding/output/`. Th We won't be doing anything with these files just yet - wait until we get to the deployment part. Along with `crowdfunding/output/`, there are a few other folders and files generated. You can safely ignore them for now, but do not delete the `/crowdfunding/wasm/` folder - it's what makes the build command faster after the initial run. The following can be safely deleted, as they are not important for this contract: + - The `scenarios/` folder; - The `crowdfunding/tests/crowdfunding_scenario_go_test.rs` file; - The `crowdfunding/tests/crowdfunding_scenario_rs_test.rs` file. @@ -208,7 +214,7 @@ It's time to add some functionality to the `init` function now. ## Step 4: persisting values -In this step, you will use the `init` method to persist some values in the storage of the Crowdfunding smart contract. +In this step, you will use the `init` method to persist some values in the storage of the Crowdfunding smart contract. [comment]: # (mx-context-auto) @@ -218,7 +224,7 @@ Every smart contract can store key-value pairs in a persistent structure, create The storage of a smart contract is, for all intents and purposes, **a generic hash map or dictionary**. When you want to store some arbitrary value, you store it under a specific key. To get the value back, you need to know the key you stored it under. -To help you keep the code clean, the framework enables you to write **setter** and **getter** methods for individual key-value pairs. There are several ways to interact with storage from a contract, but the simplest one is by using [**storage mappers**](/docs/developers/developer-reference/storage-mappers.md). +To help you keep the code clean, the framework enables you to write **setter** and **getter** methods for individual key-value pairs. There are several ways to interact with storage from a contract, but the simplest one is by using [**storage mappers**](/docs/developers/developer-reference/storage-mappers.md). Next, you will declare a [_SingleValueMapper_](/docs/developers/developer-reference/storage-mappers.md#singlevaluemapper) that has the purpose of storing a [_BigUint_](/docs/developers/best-practices/biguint-operations.md) number. This storage mapper is dedicated to storing/retrieving the value stored under the key `target`: @@ -264,10 +270,9 @@ Whenever you want to make sure your code is in order, run the build command: sc-meta all build ``` -There's one more thing: by default, none of the `fn` statements declare smart contract methods that are _externally callable_. All the data in the contract is publicly available, but it can be cumbersome to search through the contract storage manually. That is why it is often nice to make getters public, so people can call them to get specific data out. - -Public methods are annotated with either `#[endpoint]` or `#[view]`. There is currently no difference in functionality between them (but there might be at some point in the future). Semantically, `#[view]` indicates readonly methods, while `#[endpoint]` suggests that the method also changes the contract state. +There's one more thing: by default, none of the `fn` statements declare smart contract methods that are _externally callable_. All the data in the contract is publicly available, but it can be cumbersome to search through the contract storage manually. That is why it is often nice to make getters public, so people can call them to get specific data out. +Public methods are annotated with either `#[endpoint]` or `#[view]`. There is currently no difference in functionality between them (but there might be at some point in the future). Semantically, `#[view]` indicates readonly methods, while `#[endpoint]` suggests that the method also changes the contract state. ```rust #[view] @@ -284,6 +289,7 @@ You can also think of `#[init]` as a special type of endpoint. You must always make sure that the code you write functions as intended. That's what **automatic testing** is for. For now, this is how your contract looks: + ```rust #![no_std] @@ -352,6 +358,7 @@ Your folder should look like this (output from the command `tree -L 2`): ``` Before creating the first test, we need to [set up the environment](/docs/developers/testing/rust/sc-test-setup.md). We will: + 1. Generate the smart contract's proxy; 2. Register the contract; 3. Set up accounts. @@ -437,7 +444,7 @@ fn crowdfunding_deploy_test() { } ``` -In the snippet above, we've added only one account to the fictional universe of Crowdfunding smart contract. It is an account with the address `owner`, which the testing environment will use to pretend it's you. Note that in this fictional universe, your account nonce is `0` (meaning you've never used this account yet) and your `balance` is `1,000,000`. +In the snippet above, we've added only one account to the fictional universe of Crowdfunding smart contract. It is an account with the address `owner`, which the testing environment will use to pretend it's you. Note that in this fictional universe, your account nonce is `0` (meaning you've never used this account yet) and your `balance` is `1,000,000`. :::important No transaction can start if that account does not exist in the mocked blockchain. More explanations can be found [here](/docs/developers/testing/rust/sc-test-setup.md#setting-accounts). @@ -468,11 +475,11 @@ fn crowdfunding_deploy_test() { } ``` -The transaction above is a deploy call that stores in `target` value `500,000,000,000`. It was fictionally submitted by "you", using your account with the address `owner`. +The transaction above is a deploy call that stores in `target` value `500,000,000,000`. It was fictionally submitted by "you", using your account with the address `owner`. -`.new_address(CROWDFUNDING_ADDRESS)` marks that the address of the deployed contracts will be the value stored in the **CROWDFUNDING_ADDRESS** constant. +`.new_address(CROWDFUNDING_ADDRESS)` marks that the address of the deployed contracts will be the value stored in the **CROWDFUNDING_ADDRESS** constant. -`.code(CODE_PATH)` explicitly sets the deployment Crowdfunding's code source as bytes. +`.code(CODE_PATH)` explicitly sets the deployment Crowdfunding's code source as bytes. :::note Deploy calls are specified by the code source. You can find more details about what data needs a transaction [here](/docs/developers/transactions/tx-data.md). @@ -487,6 +494,7 @@ Remember to run `sc-meta all build` before running the test, especially if you m What's the purpose of testing if we do not validate the behavior of the entities interacting with the blockchain? Let's take the next step by enhancing the `crowdfunding_deploy_test()` function to include verification operations. Once the deployment is executed, we will verify if: + - The **contract address** is **CROWDFUNDING_ADDRESS**; - The **owner** has no less EGLD than the value with which it was initialized: `1,000,000`; - `target` contains the value set at deployment: `500,000,000,000`. @@ -512,7 +520,8 @@ fn crowdfunding_deploy_test() { .run(); } ``` -Notice that there are two accounts now, not just one. There's evidently the account `owner` and the new account `crowdfunding`, as a result of the deployment transaction. + +Notice that there are two accounts now, not just one. There's evidently the account `owner` and the new account `crowdfunding`, as a result of the deployment transaction. :::important Smart contracts _are_ accounts in the MultiversX Network, accounts with associated code, which can be executed when transactions are sent to them. @@ -525,7 +534,9 @@ The `.check_account(OWNER)` method verifies whether an account exists at the spe ::: :::note -The `.query()` method is used to interact with the smart contract's view functions via the proxy, retrieving information without modifying the blockchain state. Details available [here](/docs/developers/testing/rust/sc-blackbox-calls.md#query). +The `.query()` method is used to interact with the smart contract's view functions via the proxy, retrieving information without modifying the blockchain state. + +There is no caller, no payment, and gas price/gas limit. On the real blockchain, a smart contract query does not create a transaction on the blockchain, so no account is needed. Details available [here](/docs/developers/testing/rust/sc-blackbox-calls.md#query). ::: ## Run test diff --git a/docs/developers/tutorials/crowdfunding-p2.md b/docs/developers/tutorials/crowdfunding-p2.md index 2731b6ed9..ade86fa8d 100644 --- a/docs/developers/tutorials/crowdfunding-p2.md +++ b/docs/developers/tutorials/crowdfunding-p2.md @@ -1,325 +1,400 @@ --- id: crowdfunding-p2 -title: The Crowdfunding Smart Contract (part 2) +title: Enhancing the Crowdfunding Smart Contract --- [comment]: # (mx-abstract) Define contract arguments, handle storage, process payments, define new types, write better tests [comment]: # (mx-context-auto) -# **Configuring the contract** +## Configuring the contract -The previous chapter left us with a minimal contract as a starting point. +[The previous chapter](/docs/developers/tutorials/crowdfunding-p1.md) left us with a minimal contract as a starting point. The first thing we need to do is to configure the desired target amount and the deadline. The deadline will be expressed as the block timestamp after which the contract can no longer be funded. We will be adding 2 more storage fields and arguments to the constructor. ```rust - #[view(getTarget)] - #[storage_mapper("target")] - fn target(&self) -> SingleValueMapper; - - #[view(getDeadline)] - #[storage_mapper("deadline")] - fn deadline(&self) -> SingleValueMapper; - - #[view(getDeposit)] - #[storage_mapper("deposit")] - fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper; - - #[init] - fn init(&self, target: BigUint, deadline: u64) { - self.target().set(&target); - self.deadline().set(&deadline); - } +#[view(getTarget)] +#[storage_mapper("target")] +fn target(&self) -> SingleValueMapper; + +#[view(getDeadline)] +#[storage_mapper("deadline")] +fn deadline(&self) -> SingleValueMapper; + +#[view(getDeposit)] +#[storage_mapper("deposit")] +fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper; + +#[init] +fn init(&self, target: BigUint, deadline: u64) { + self.target().set(&target); + self.deadline().set(&deadline); +} ``` -The deadline being a block timestamp can be expressed as a regular 64-bits unsigned int. The target, however, being a sum of EGLD cannot. Note that 1 EGLD = 10^18 EGLD-wei (also known as atto-EGLD), the smallest unit of currency, and all payments are expressed in wei. So you can see that even for small payments the numbers get large. Luckily, the framework offers support for big numbers out of the box. Two types are available: BigUint and BigInt. +The deadline being a block timestamp can be expressed as a regular 64-bits unsigned integer. The target, however, being a sum of EGLD cannot. + +:::note + 1 EGLD = 1018 EGLD-wei, also known as atto-EGLD. + +It is the smallest unit of currency, and all payments are expressed in wei. +::: + +Even for small payments, the numbers get large. Luckily, the framework offers support for big numbers out of the box. Two types are available: [**BigUint**](/docs/developers/best-practices/biguint-operations.md) and **BigInt**. -Try to avoid the signed version as much as possible (unless negative values are really possible and needed). There are some caveats with BigInt argument serialization that can lead to subtle bugs. +:::tip +Try to **avoid** using the signed version whenever possible, unless negative values are truly needed. There are some caveats with **BigInt** argument serialization that can lead to **subtle bugs**. +::: -Also note that BigUint logic does not reside in the contract, but is built into the MultiversX VM API, to not bloat the contract code. +Note that BigUint logic is not implemented within the contract itself but is provided by the MultiversX VM API to keep the contract code lightweight. Let's test that initialization works. -```json title=crowdfunding-init.scen.json -{ - "name": "crowdfunding deployment test", - "steps": [ - { - "step": "setState", - "accounts": { - "address:my_address": { - "nonce": "0", - "balance": "1,000,000" - } - }, - "newAddresses": [ - { - "creatorAddress": "address:my_address", - "creatorNonce": "0", - "newAddress": "sc:crowdfunding" - } - ] - }, - { - "step": "scDeploy", - "txId": "deploy", - "tx": { - "from": "address:my_address", - "contractCode": "file:../output/crowdfunding.wasm", - "arguments": [ - "500,000,000,000", - "123,000" - ], - "gasLimit": "5,000,000", - "gasPrice": "0" - }, - "expect": { - "out": [], - "status": "0", - "gas": "*", - "refund": "*" - } - }, - { - "step": "checkState", - "accounts": { - "address:my_address": { - "nonce": "1", - "balance": "1,000,000", - "storage": {} - }, - "sc:crowdfunding": { - "nonce": "0", - "balance": "0", - "storage": { - "str:target": "500,000,000,000", - "str:deadline": "123,000" - }, - "code": "file:../output/crowdfunding.wasm" - } - } - } - ] +First, navigate to the contract's crate path and rebuild it using: + +```bash +sc-meta all build +``` + +Next, we regenerate the proxy at the same path using: + +```bash +sc-meta all proxy +``` + +Finally, we update the test: + +```rust title=crowdfunding_blackbox_test.rs +#[test] +fn crowdfunding_deploy_test() { + let mut world = world(); + + world.account(OWNER).nonce(0).balance(1000000); + + let crowdfunding_address = world + .tx() + .from(OWNER) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .init(500_000_000_000u64, 123000u64) + .code(CODE_PATH) + .new_address(CROWDFUNDING_ADDRESS) + .returns(ReturnsNewAddress) + .run(); + + assert_eq!(crowdfunding_address, CROWDFUNDING_ADDRESS.to_address()); + + world.check_account(OWNER).balance(1_000_000); + + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .target() + .returns(ExpectValue(500_000_000_000u64)) + .run(); + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .deadline() + .returns(ExpectValue(123000u64)) + .run(); } ``` -Note the added `"arguments"` field in `scDeploy` and the added fields in storage. +Note the added arguments in the deploy call and the additional query for the `deadline` storage. -Run the tests again: +Run the test again from the contract crate's path: ```bash -sc-meta all build -cargo test +sc-meta test ``` [comment]: # (mx-context-auto) -## **Funding the contract** +## Funding the contract It is not enough to receive the funds, the contract also needs to keep track of who donated how much. ```rust - #[view(getDeposit)] - #[storage_mapper("deposit")] - fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper; - - #[endpoint] - #[payable("EGLD")] - fn fund(&self) { - let payment = self.call_value().egld_value(); - let caller = self.blockchain().get_caller(); - self.deposit(&caller).update(|deposit| *deposit += &*payment); - } +#[view(getDeposit)] +#[storage_mapper("deposit")] +fn deposit(&self, donor: &ManagedAddress) -> SingleValueMapper; + +#[endpoint] +#[payable("EGLD")] +fn fund(&self) { + let payment = self.call_value().egld(); + let caller = self.blockchain().get_caller(); + self.deposit(&caller).update(|deposit| *deposit += &*payment); +} ``` +:::tip +Every time the contract is modified, you need to rebuild it and regenerate the proxy. +::: + A few things to unpack: 1. This storage mapper has an extra argument, for an address. This is how we define a map in the storage. The donor argument will become part of the storage key. Any number of such key arguments can be added, but in this case we only need one. The resulting storage key will be a concatenation of the specified base key `"deposit"` and the serialized argument. -2. We encounter the first payable function. By default, any function in a smart contract is not payable, i.e. sending a sum of EGLD to the contract using the function will cause the transaction to be rejected. Payable functions need to be annotated with #[payable]. -3. fund needs to also be explicitly declared as an endpoint. All `#[payable]`methods need to be marked `#[endpoint]`, but not the other way around. +2. We encounter the first payable function. By default, any function in a smart contract is not payable, i.e. sending a sum of EGLD to the contract using the function will cause the transaction to be rejected. Payable functions need to be annotated with `#[payable]`. +3. `fund` needs to also be explicitly declared as an endpoint. All `#[payable]`methods need to be marked `#[endpoint]`, but not the other way around. -To test the function, we'll add a new test file, in the same `scenarios` folder. Let's call it `crowdfunding-fund.scen.json` . +To test the function, we will add a new test, in the same `crowdfunding_blackbox_test.rs` file. Let's call it `crowdfunding_fund_test()` . -To avoid duplicating the deployment code, we import it from `crowdfunding-init.scen.json` . +To avoid duplicate code, we will put all the deployment and account setup logic into a function called `crowdfunding_deploy()`. This function will return a **ScenarioWorld** response, which gives us the **state of the mocked chain** after setting up an account with the OWNER address and deploying the crowdfunding contract. -```json title=crowdfunding-fund.scen.json -{ - "name": "crowdfunding funding", - "steps": [ - { - "step": "externalSteps", - "path": "crowdfunding-init.scen.json" - }, - { - "step": "setState", - "accounts": { - "address:donor1": { - "nonce": "0", - "balance": "400,000,000,000" - } - } - }, - { - "step": "scCall", - "txId": "fund-1", - "tx": { - "from": "address:donor1", - "to": "sc:crowdfunding", - "egldValue": "250,000,000,000", - "function": "fund", - "arguments": [], - "gasLimit": "100,000,000", - "gasPrice": "0" - }, - "expect": { - "out": [], - "status": "", - "gas": "*", - "refund": "*" - } - }, - { - "step": "checkState", - "accounts": { - "address:my_address": { - "nonce": "1", - "balance": "1,000,000", - "storage": {} - }, - "address:donor1": { - "nonce": "1", - "balance": "150,000,000,000", - "storage": {} - }, - "sc:crowdfunding": { - "nonce": "0", - "balance": "250,000,000,000", - "storage": { - "str:target": "500,000,000,000", - "str:deadline": "123,000", - "str:deposit|address:donor1": "250,000,000,000" - }, - "code": "file:../output/crowdfunding.wasm" - } - } - } - ] +```rust title=crowdfunding_blackbox_test.rs +fn crowdfunding_deploy() -> ScenarioWorld { + let mut world = world(); + + world.account(OWNER).nonce(0).balance(1000000); + + let crowdfunding_address = world + .tx() + .from(OWNER) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .init(500_000_000_000u64, 123000u64) + .code(CODE_PATH) + .new_address(CROWDFUNDING_ADDRESS) + .returns(ReturnsNewAddress) + .run(); + + assert_eq!(crowdfunding_address, CROWDFUNDING_ADDRESS.to_address()); + + world } ``` +Now that we've moved the deployment logic to a separate function, let's update the test that checks the deploy endpoint like this: + +```rust title=crowdfunding_blackbox_test.rs +#[test] +fn crowdfunding_deploy_test() { + let mut world = crowdfunding_deploy(); + world.check_account(OWNER).balance(1_000_000); + + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .target() + .returns(ExpectValue(500_000_000_000u64)) + .run(); + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .deadline() + .returns(ExpectValue(123000u64)) + .run(); +} +``` + +With the code organized, we can now start developing the test for the fund endpoint. + +```rust title=crowdfunding_blackbox_test.rs +const DONOR: TestAddress = TestAddress::new("donor"); + +fn crowdfunding_fund() -> ScenarioWorld { + let mut world = deploy_crowdfunding(); + + world.account(DONOR).nonce(0).balance(400_000_000_000u64); + + world + .tx() + .from(DONOR) + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .fund() + .egld(250_000_000_000u64) + .run(); + + world +} + +#[test] +fn crowdfunding_fund_test() { + let mut world = crowdfunding_fund(); + + world.check_account(OWNER).nonce(1).balance(1_000_000u64); + world + .check_account(DONOR) + .nonce(1) + .balance(150_000_000_000u64); + world + .check_account(CROWDFUNDING_ADDRESS) + .nonce(0) + .balance(250_000_000_000u64); + + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .target() + .returns(ExpectValue(500_000_000_000u64)) + .run(); + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .deadline() + .returns(ExpectValue(123_000u64)) + .run(); + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .deposit(DONOR) + .returns(ExpectValue(250_000_000_000u64)) + .run(); +} + +``` + Explanation: -1. `"externalSteps"`allows us to import steps from another json file. This is very handy, because we can write test scenarios that branch out from each other without having to duplicate code. Here we will be reusing the deployment steps in all tests. These imported steps get executed again each time they are imported. -2. We need a donor, so we add another account using a new `"setState"` step. -3. The actual simulated transaction. Note that we use `"scCall"` instead of `"scDeploy"`. There is a `"to"` field, and no `"contractCode"`. The rest functions the same. The `"egldValue"` field indicates the amount paid to the function. -4. When checking the state, we have a new user, we see that the donor's balance is decreased by the amount paid, and the contract balance increased by the same amount. -5. There is another entry in the contract storage. The pipe symbol`|`in the key means concatenation. The addresses are serialized as itself, and we can represent it in the same readable format. +1. We need a **donor**, so we add another account using `.account(DONOR)`. +2. The simulated transaction includes: + - [Sender](/docs/developers/transactions/tx-from.md): `.from(DONOR)` + - [Receiver](/docs/developers/transactions/tx-to.md): `.to(CROWDFUNDING_ADDRESS)`. +3. The [payment](/docs/developers/transactions/tx-payment.md) in the transaction is made using `.egld(250_000_000_000u64)`. +4. When checking the state, we see that the **donor's balance is decreased** by the amount paid, and the **contract balance increased** by the same amount. -Test it by running the commands again: +Run again the following command in the root of the project to test it: -```python -sc-meta all build -cargo test +```bash +sc-meta test ``` You should then see that both tests pass: -```python -Scenario: crowdfunding-fund.scen.json ... ok -Scenario: crowdfunding-init.scen.json ... ok -Done. Passed: 2. Failed: 0. Skipped: 0. -SUCCESS +```bash +Running tests in ./ ... +Executing cargo test ... + Compiling crowdfunding v0.0.0 (/home/crowdfunding) + Finished `test` profile [unoptimized + debuginfo] target(s) in 0.22s + Running unittests src/crowdfunding.rs (target/debug/deps/crowdfunding-73d2b98f9e2cff29) + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + + Running tests/crowdfunding_blackbox_test.rs (target/debug/deps/crowdfunding_blackbox_test-19b9f0d2428bc9f9) + +running 2 tests +test crowdfunding_deploy_test ... ok +test crowdfunding_fund_test ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s + + Doc-tests crowdfunding + +running 0 tests + +test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s + +Process finished with: exit status: 0 ``` [comment]: # (mx-context-auto) -## **Validation** +## Validation -It doesn't make sense to fund after the deadline has passed, so fund transactions after a certain block timestamp must be rejected. The idiomatic way to do this is: +It doesn't make sense to create a funding what has the target 0 or a negative number, so target needs to be more than 0. Similarly, it’s unreasonable to create a fundraiser with a deadline in the past, so the deadline must be in the future relative to when the contract is deployed. ```rust - #[endpoint] - #[payable("EGLD")] - fn fund(&self) { - let payment = self.call_value().egld_value(); +#[init] +fn init(&self, target: BigUint, deadline: u64) { + require!(target > 0, "Target must be more than 0"); + self.target().set(target); + + require!( + deadline > self.get_current_time(), + "Deadline can't be in the past" + ); + self.deadline().set(deadline); +} +``` - let current_time = self.blockchain().get_block_timestamp(); - require!(current_time < self.deadline().get(), "cannot fund after deadline"); +Additionally, it doesn't make sense to accept funding after the deadline has passed, so any fund transactions after a certain block timestamp should be rejected. The idiomatic way to handle this is: - let caller = self.blockchain().get_caller(); - self.deposit(&caller).update(|deposit| *deposit += &*payment); - } +```rust +#[endpoint] +#[payable("EGLD")] +fn fund(&self) { + let payment = self.call_value().egld(); + + let current_time = self.blockchain().get_block_timestamp(); + require!(current_time < self.deadline().get(), "cannot fund after deadline"); + + let caller = self.blockchain().get_caller(); + self.deposit(&caller).update(|deposit| *deposit += &*payment); +} ``` :::tip -`require!(expression, error_msg)` is the same as `if !expression { sc_panic!(error_msg) }` - -`sc_panic!("message")` works similarly to the standard `panic!`, but works better in a smart contract context and is more efficient. The regular `panic!` is allowed too, but it might bloat your code, and you won't see the error message. +The [`require!`](/docs/developers/developer-reference/sc-messages.md#require) macro is used for enforcing conditions. ::: -We'll create another test file to verify that the validation works: `test-fund-too-late.scen.json` . +We will create another test to verify that the validation works: `crowdfunding_fund_too_late_test()` . -```json title=crowdfunding-fund-too-late.scen.json -{ - "name": "trying to fund one block too late", - "steps": [ - { - "step": "externalSteps", - "path": "crowdfunding-fund.scen.json" - }, - { - "step": "setState", - "currentBlockInfo": { - "blockTimestamp": "123,001" - } - }, - { - "step": "scCall", - "txId": "fund-too-late", - "tx": { - "from": "address:donor1", - "to": "sc:crowdfunding", - "egldValue": "10,000,000,000", - "function": "fund", - "arguments": [], - "gasLimit": "100,000,000", - "gasPrice": "0" - }, - "expect": { - "out": [], - "status": "4", - "message": "str:cannot fund after deadline", - "gas": "*", - "refund": "*" - } - } - ] +```rust title=crowdfunding_blackbox_test.rs +#[test] +fn crowdfunding_fund_too_late_test() { + let mut world = crowdfunding_fund(); + + world.current_block().block_timestamp(123_001u64); + + world + .tx() + .from(DONOR) + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .fund() + .egld(10_000_000_000u64) + .with_result(ExpectError(4, "cannot fund after deadline")) + .run(); } ``` -We branch this time from `crowdfunding-fund.scen.json`, where we already had a donor. Now the same donor wants to donate, again, but in the meantime the current block timestamp has become 123,001, one block later than the deadline. The transaction fails with status 4 (user error - all errors from within the contract will return this status). The testing environment allows us to also check that the correct message was returned. +Now the same donor wants to donate, again, but in the meantime the current block timestamp has become `123_001`, one block later than the deadline. + +The transaction **fails** with status 4. The testing environment allows us to also check that the proper error message was returned. + +:::info +Status 4 indicates a user error. All errors originating within the contract will return this status. +::: + +By testing the contract again, you should see that all three tests pass: -By building and testing the contract again, you should see that all three tests pass: +```bash +Running tests/crowdfunding_blackbox_test.rs (target/debug/deps/crowdfunding_blackbox_test-19b9f0d2428bc9f9) -```python -Scenario: crowdfunding-fund-too-late.scen.json ... ok -Scenario: crowdfunding-fund.scen.json ... ok -Scenario: crowdfunding-init.scen.json ... ok -Done. Passed: 3. Failed: 0. Skipped: 0. -SUCCESS +running 3 tests +test crowdfunding_deploy_test ... ok +test crowdfunding_fund_test ... ok +test crowdfunding_fund_too_late_test ... ok + +test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.01s ``` [comment]: # (mx-context-auto) -## **Querying for the contract status** +## Querying for the contract status + +The contract status can be known by anyone by looking into the storage and on the blockchain, but it is really inconvenient right now. -The contract status can be known by anyone by looking into the storage and on the blockchain, but it is really inconvenient right now. Let's create an endpoint that gives this status directly. The status will be one of: `FundingPeriod`, `Successful` or `Failed`. We could use a number to represent it in code, but the nice way to do it is with an enum. We will take this opportunity to show how to create a serializable type that can be taken as argument, returned as result or saved in storage. +Let's create an endpoint that gives this status directly. The status will be one of: `FundingPeriod`, `Successful` or `Failed`. + +We could use a number to represent it in code, but the nice way to do it is with an enum. We will take this opportunity to show how to create a serializable type that can be taken as argument, returned as result or saved in storage. This is the enum: ```rust -#[derive(TopEncode, TopDecode, TypeAbi, PartialEq, Clone, Copy)] +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Clone, Copy)] pub enum Status { FundingPeriod, Successful, @@ -328,145 +403,135 @@ pub enum Status { ``` Make sure to add it outside the contract trait. -Don't forget to add the import for the derive types. This can be place on top off the file next to the other import. + +Don't forget to add the import for the derive types. This can be place on top off the file, replacing `use multiversx_sc::imports::*;` with: ```rust -multiversx_sc::derive_imports!(); +use multiversx_sc::{derive_imports::*, imports::*}; ``` + The `#[derive]` keyword in Rust allows you to automatically implement certain traits for your type. `TopEncode` and `TopDecode` mean that objects of this type are serializable, which means they can be interpreted from/to a string of bytes. -`TypeAbi` is needed to export the type when you want to interact with the already deployed contract. This is out of scope of this tutorial though. +`#[type_abi]` is needed to export the type when you want to interact with the already deployed contract. This is out of scope of this tutorial though. -`PartialEq`, `Clone` and `Copy` are Rust traits that allow your type instances to be compared with the `==` operator, and the `Clone` and `Copy` traits allow your object instances to be clone/copied respectively. +`PartialEq`, `Clone` and `Copy` are Rust traits. `PartialEq` trait allows your type instances to be compared with the `==` operator. `Clone` and `Copy` traits allow your object instances to be clone/copied respectively. -We can now use the type Status just like we use the other types, so we can write the following method in the contract trait: +We can now use the type **Status** just like we use the other types, so we can write the following method in the contract trait: ```rust - #[view] - fn status(&self) -> Status { - if self.blockchain().get_block_timestamp() <= self.deadline().get() { - Status::FundingPeriod - } else if self.get_current_funds() >= self.target().get() { - Status::Successful - } else { - Status::Failed - } - } - - #[view(getCurrentFunds)] - fn get_current_funds(&self) -> BigUint { - self.blockchain().get_sc_balance(&EgldOrEsdtTokenIdentifier::egld(), 0) - } +#[view] +fn status(&self) -> Status { + if self.blockchain().get_block_timestamp() <= self.deadline().get() { + Status::FundingPeriod + } else if self.get_current_funds() >= self.target().get() { + Status::Successful + } else { + Status::Failed + } +} + +#[view(getCurrentFunds)] +fn get_current_funds(&self) -> BigUint { + self.blockchain().get_sc_balance(&EgldOrEsdtTokenIdentifier::egld(), 0) +} ``` -To test this method, we append one more step to the last test we worked on, `test-fund-too-late.scen.json` : - -```json -{ - "name": "trying to fund one block too late", - "steps": [ - { - "step": "externalSteps", - "path": "crowdfunding-fund.scen.json" - }, - { - "step": "setState", - "currentBlockInfo": { - "blockTimestamp": "123,001" - } - }, - { - "step": "scCall", - "txId": "fund-too-late", - "tx": { - "from": "address:donor1", - "to": "sc:crowdfunding", - "egldValue": "10,000,000,000", - "function": "fund", - "arguments": [], - "gasLimit": "100,000,000", - "gasPrice": "0" - }, - "expect": { - "out": [], - "status": "4", - "message": "str:cannot fund after deadline", - "gas": "*", - "refund": "*" - } - }, - { - "step": "scQuery", - "txId": "check-status", - "tx": { - "to": "sc:crowdfunding", - "function": "status", - "arguments": [] - }, - "expect": { - "out": ["2"], - "status": "0" - } - } - ] +We will also modify the `require` condition in the `fund` endpoint to ensure that the deposit can only be made during the **FundingPeriod**. + +```rust +#[endpoint] +#[payable("EGLD")] +fn fund(&self) { + let payment = self.call_value().egld(); + + require!( + self.status() == Status::FundingPeriod, + "cannot fund after deadline" + ); + + let caller = self.blockchain().get_caller(); + self.deposit(&caller) + .update(|deposit| *deposit += &*payment); } ``` -Since the function we're trying to call is a view function, we use the `scQuery` step instead of the `scCall` step. The difference is that for `scQuery`, there is no `caller`, no payment, and gas price/gas limit. On the real blockchain, a smart contract query does not create a transaction on the blockchain, so no account is needed. `scQuery` simulates this exact behaviour. +To test `status` method, we update the last test we worked on, `crowdfunding_fund_too_late_test()`: -Note the call to "status" at the end and the result `"out": [ "2" ]` , which is the encoding for `Status::Failure`. Enums are encoded as an index of their values. In this example, `Status::FundingPeriod` is `"0"` (or `""`), `Status::Successful` is `"1"` and, as you've already seen, `Status::Failure` is `"2"`. +```rust +use crowdfunding::crowdfunding_proxy::{self, Status}; + +#[test] +fn crowdfunding_fund_too_late_test() { + /* + Code before updating + */ + + world + .query() + .to(CROWDFUNDING_ADDRESS) + .typed(crowdfunding_proxy::CrowdfundingProxy) + .status() + .returns(ExpectValue(Status::Failed)) + .run(); +} +``` -Contract functions can return in principle any number of results, that is why `"out"` is a list. +:::info +The return response checked in the query is an enum defined in the crowdfunding proxy, thus you have to import `crowdfunding_proxy::Status`. +::: [comment]: # (mx-context-auto) -## **Claim functionality** +## Claim functionality Finally, let's add the `claim` method. The `status` method we just implemented helps us keep the code tidy: ```rust - #[endpoint] - fn claim(&self) { - match self.status() { - Status::FundingPeriod => sc_panic!("cannot claim before deadline"), - Status::Successful => { - let caller = self.blockchain().get_caller(); - require!( - caller == self.blockchain().get_owner_address(), - "only owner can claim successful funding" - ); - - let sc_balance = self.get_current_funds(); - self.send().direct_egld(&caller, &sc_balance); - }, - Status::Failed => { - let caller = self.blockchain().get_caller(); - let deposit = self.deposit(&caller).get(); +#[endpoint] +fn claim(&self) { + match self.status() { + Status::FundingPeriod => sc_panic!("cannot claim before deadline"), + Status::Successful => { + let caller = self.blockchain().get_caller(); + require!( + caller == self.blockchain().get_owner_address(), + "only owner can claim successful funding" + ); + + let sc_balance = self.get_current_funds(); + self.send().direct_egld(&caller, &sc_balance); + }, + Status::Failed => { + let caller = self.blockchain().get_caller(); + let deposit = self.deposit(&caller).get(); - if deposit > 0u32 { - self.deposit(&caller).clear(); - self.send().direct_egld(&caller, &deposit); - } - }, - } + if deposit > 0u32 { + self.deposit(&caller).clear(); + self.send().direct_egld(&caller, &deposit); + } + }, } +} ``` -The only new function here is `self.send().direct_egld()`, which simply forwards EGLD from the contract to the given address. +[`sc_panic!`](/docs/developers/developer-reference/sc-messages.md) has the same functionality as [`panic!`](https://doc.rust-lang.org/std/macro.panic.html) from Rust, with the difference that it works in a no_std environment. + +`self.send().direct_egld()` forwards EGLD from the contract to the given address. [comment]: # (mx-context-auto) -## **The final contract code** +## The final contract code If you followed all the steps presented until now, you should have ended up with a contract that looks something like: -```rust title=final.rs +```rust title=crowdfunding.rs #![no_std] -multiversx_sc::imports!(); -multiversx_sc::derive_imports!(); +use multiversx_sc::{derive_imports::*, imports::*}; +pub mod crowdfunding_proxy; -#[derive(TopEncode, TopDecode, TypeAbi, PartialEq, Eq, Clone, Copy, Debug)] +#[type_abi] +#[derive(TopEncode, TopDecode, PartialEq, Clone, Copy)] pub enum Status { FundingPeriod, Successful, @@ -490,7 +555,7 @@ pub trait Crowdfunding { #[endpoint] #[payable("EGLD")] fn fund(&self) { - let payment = self.call_value().egld_value(); + let payment = self.call_value().egld(); require!( self.status() == Status::FundingPeriod, @@ -498,7 +563,7 @@ pub trait Crowdfunding { ); let caller = self.blockchain().get_caller(); - self.deposit(&caller).update(|deposit| *deposit += &*payment); + self.deposit(&caller).update(|deposit| *deposit += &*payment); } #[view] @@ -569,14 +634,6 @@ As an exercise, try to add some more tests, especially ones involving the claim [comment]: # (mx-context-auto) -## **Next steps** - -This concludes the first Rust multiversx-sc tutorial. +## Next steps -For more detailed documentation, visit our Rust docs on [docs.rs](https://docs.rs/multiversx-sc/latest/multiversx_sc/). - -If you want to see some other smart contract examples, or even an extended version of the crowdfunding smart contract, you can check here: https://github.com/multiversx/mx-sdk-rs/tree/master/contracts/examples - -:::tip -When entering directly on the `multiversx-sc` repository on GitHub, you will first see the `master` branch. While this is at all times the latest version of the contracts, they might sometimes rely on unreleased features and therefore not compile outside of the repository. Getting the examples from the last released version is, however, always safe. -::: +If you want to see some other smart contract examples, or even an extended version of the crowdfunding smart contract, you can check [here](https://github.com/multiversx/mx-contracts-rs). From 0fd4f536085d62cc854d4d9b0795c518de4c39a7 Mon Sep 17 00:00:00 2001 From: BiancaIalangi Date: Fri, 21 Feb 2025 18:54:23 +0200 Subject: [PATCH 4/9] fix path --- docs/developers/overview.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/developers/overview.md b/docs/developers/overview.md index 7f549d66c..96f3da174 100644 --- a/docs/developers/overview.md +++ b/docs/developers/overview.md @@ -40,7 +40,7 @@ Below is a list of tutorials for building on MultiversX: | [Build your first dApp in 15 minutes](/developers/tutorials/your-first-dapp) | Video + written tutorial on how to create your first dApp. | | [Cryptozombies Tutorials](https://cryptozombies.io/en/multiversx) | Interactive way of learning how to write MultiversX Smart Contracts. | | [Build a microservice for your dApp](/developers/tutorials/your-first-microservice) | Video + written tutorial on how to create your microservice. | -| [Building a Crowdfunding Smart Contract](/docs/developers/tutorials/crowdfunding-p2.md) | Write, build, and test a simple smart contract. | +| [Building a Crowdfunding Smart Contract](/docs/developers/tutorials/crowdfunding-p1.md) | Write, build and test a simple smart contract. | | [Enhancing the Crowdfunding Smart Contract](/docs/developers/tutorials/crowdfunding-p2.md) | Expand and refine the functionality of an existing contract.| | [Staking contract Tutorial](/developers/tutorials/staking-contract) | Step by step tutorial on how to create a Staking Smart Contract. | | [Energy DAO Tutorial](/developers/tutorials/energy-dao) | In depth analysis of the Energy DAO SC template. | From c3efc42129504cdc510dc50cb872cca01febbb7a Mon Sep 17 00:00:00 2001 From: bogdan-rosianu <51945539+bogdan-rosianu@users.noreply.github.com> Date: Wed, 19 Mar 2025 14:18:30 +0200 Subject: [PATCH 5/9] Update fungible-tokens.mdx --- docs/tokens/fungible-tokens.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tokens/fungible-tokens.mdx b/docs/tokens/fungible-tokens.mdx index fff5a7e6b..21866f907 100644 --- a/docs/tokens/fungible-tokens.mdx +++ b/docs/tokens/fungible-tokens.mdx @@ -85,7 +85,7 @@ Because of the 6 random characters sequence (3 bytes - 6 characters hex encoded) Token Name: -- length between 3 and 50 characters +- length between 3 and 20 characters - alphanumeric characters only Token Ticker: From 0b18fad3508b06a5d8d0543b7e3ffeceb117def4 Mon Sep 17 00:00:00 2001 From: BiancaIalangi Date: Thu, 20 Mar 2025 12:08:27 +0200 Subject: [PATCH 6/9] apply reviews --- docs/developers/tutorials/crowdfunding-p1.md | 6 +++--- docs/developers/tutorials/crowdfunding-p2.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/developers/tutorials/crowdfunding-p1.md b/docs/developers/tutorials/crowdfunding-p1.md index 87c8b43b0..f6afac2cc 100644 --- a/docs/developers/tutorials/crowdfunding-p1.md +++ b/docs/developers/tutorials/crowdfunding-p1.md @@ -21,7 +21,7 @@ The idea is simple: the smart contract will accept transfers until a deadline is If the deadline is reached and the smart contract has gathered an amount of EGLD above the desired funds, then the smart contract will consider the crowdfunding a success, and it will consequently send all the EGLD to a predetermined account (yours!). -However, if the donations fall short of the target, the contract will return all the EGLDs to the donors. +However, if the donations fall short of the target, the contract will return all the all EGLD tokens to the donors. [comment]: # (mx-context-auto) @@ -286,7 +286,7 @@ You can also think of `#[init]` as a special type of endpoint. ## Step 5: testing -You must always make sure that the code you write functions as intended. That's what **automatic testing** is for. +You must always make sure that the code you write functions as intended. That's what **automated testing** is for. For now, this is how your contract looks: @@ -452,7 +452,7 @@ No transaction can start if that account does not exist in the mocked blockchain ### Deploy -The purpose of the account created one step ago is to act as the owner of the Crowdfunding smart contract. To make this happen, the **OWNER** constant will serve as the transaction **sender**. +The purpose of the account created previously is to act as the owner of the Crowdfunding smart contract. To make this happen, the **OWNER** constant will serve as the transaction **sender**. ```rust const CROWDFUNDING_ADDRESS: TestSCAddress = TestSCAddress::new("crowdfunding"); diff --git a/docs/developers/tutorials/crowdfunding-p2.md b/docs/developers/tutorials/crowdfunding-p2.md index ade86fa8d..516a4a8bc 100644 --- a/docs/developers/tutorials/crowdfunding-p2.md +++ b/docs/developers/tutorials/crowdfunding-p2.md @@ -302,7 +302,7 @@ Process finished with: exit status: 0 ## Validation -It doesn't make sense to create a funding what has the target 0 or a negative number, so target needs to be more than 0. Similarly, it’s unreasonable to create a fundraiser with a deadline in the past, so the deadline must be in the future relative to when the contract is deployed. +It doesn't make sense to create a funding that has the target 0 or a negative number, so target needs to be more than 0. Similarly, it’s unreasonable to create a fundraiser with a deadline in the past, so the deadline must be in the future relative to when the contract is deployed. ```rust #[init] From 9da46d30a9b60c94c0eb25524cd1d4f1bdb2cc81 Mon Sep 17 00:00:00 2001 From: BiancaIalangi Date: Thu, 20 Mar 2025 12:12:54 +0200 Subject: [PATCH 7/9] update req and framework version --- docs/developers/tutorials/crowdfunding-p1.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/developers/tutorials/crowdfunding-p1.md b/docs/developers/tutorials/crowdfunding-p1.md index f6afac2cc..2052114be 100644 --- a/docs/developers/tutorials/crowdfunding-p1.md +++ b/docs/developers/tutorials/crowdfunding-p1.md @@ -52,7 +52,7 @@ Automated testing is exceptionally important for the development of smart contra :::important Before starting this tutorial, make sure you have the following: -- `stable` **Rust** version `≥ 1.78.0` (install via [rustup](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) +- `stable` **Rust** version `≥ 1.83.0` (install via [rustup](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) - `sc-meta` (install [multiversx-sc-meta](/docs/sdk-and-tools/troubleshooting/rust-setup.md#installing-rust-and-sc-meta)) ::: @@ -88,13 +88,13 @@ authors = ["you"] path = "src/crowdfunding.rs" [dependencies.multiversx-sc] -version = "0.56.1" +version = "0.57.0" [dev-dependencies] num-bigint = "0.4" [dev-dependencies.multiversx-sc-scenario] -version = "0.56.1" +version = "0.57.0" [workspace] members = [ From dbac8dfcf8380de744e6c4887d1aa2f99f70f623 Mon Sep 17 00:00:00 2001 From: BiancaIalangi Date: Thu, 20 Mar 2025 12:49:25 +0200 Subject: [PATCH 8/9] update links --- .../tutorials/your-first-microservice.md | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/developers/tutorials/your-first-microservice.md b/docs/developers/tutorials/your-first-microservice.md index c08720805..eb9d09416 100644 --- a/docs/developers/tutorials/your-first-microservice.md +++ b/docs/developers/tutorials/your-first-microservice.md @@ -20,7 +20,7 @@ This guide has been made available in video format as well: ## Ping Pong Microservice -This guide extends the decentralized app we have built in our previous guide [**Build a dApp in 15 minutes**](/developers/tutorials/your-first-dapp). If you haven't followed it so far, [please do it now](https://www.youtube.com/watch?v=IdkgvlK3rb8). +This guide extends the decentralized app we have built in our previous guide [**Build a dApp in 15 minutes**](/developers/tutorials/your-first-dapp). If you haven't followed it so far, please do it now. In this guide we're going to build a microservice (an API), which is an intermediary layer between the blockchain layer and the app layer. Our app will consume this microservice instead of making requests directly on the blockchain. @@ -97,7 +97,7 @@ We'll find a configuration file specific for every network we want to deploy the First we're going to configure the redis server url. If we run a redis-server on the same machine (or on our development machine) then we can leave the default value. -Now we'll move on to the smart contract address. We can find it in our `dapp` repository (if we followed the previous guide ["Build a dApp in 15 minutes"](https://www.youtube.com/watch?v=IdkgvlK3rb8)). If you don't have a smart contract deployed on devnet, then we suggest to follow the previous guide first and then get back to this step. +Now we'll move on to the smart contract address. We can find it in our `dapp` repository (if we followed the previous guide ["Build a dApp in 15 minutes"](/docs/developers/tutorials/your-first-dapp.md)). If you don't have a smart contract deployed on devnet, then we suggest to follow the previous guide first and then get back to this step. Set the `contracts.pingPong` key with the value for the smart contract address and we're done with configuring the microservice. @@ -107,13 +107,13 @@ Set the `contracts.pingPong` key with the value for the smart contract address a We'll install the dependencies using npm -``` +``` sh npm install ``` and then we will start the microservice for the devnet: -``` +``` sh npm run start:devnet ``` @@ -211,29 +211,29 @@ We can now verify that on the dashboard we still have the countdown and the Pong You can also find the complete code on our public repository for the dApp in the branch `microservice`: -``` +```sh https://github.com/multiversx/mx-template-dapp/blob/microservice/src/pages/Dashboard/Actions/index.tsx ``` [comment]: # (mx-context-auto) -### Let's deep dive into the microservice code and explain the 2 basic features we implemented. +## Let's deep dive into the microservice code and explain the 2 basic features we implemented We want to minimize the number of requests done directly on the blockchain because of the overhead they incur, so we'll first read the time to `pong` from the blockchain, we'll cache that value and all the subsequent reads will be done from the cache. That value won't change over time. It will only reset AFTER we `pong`. [comment]: # (mx-context-auto) -## The Cache +### The Cache So the caching part is done in -``` +```sh ping-pong/microservice/src/endpoints/ping.pong/ping.pong.controller.ts ``` which uses -``` +```sh ping-pong/microservice/src/endpoints/ping.pong/ping.pong.service.ts ``` @@ -257,7 +257,7 @@ The function `this.getPongDeadlineRaw` will invoke the only read action on the b [comment]: # (mx-context-auto) -## The Transaction Processor +### The Transaction Processor After the user clicks the `Pong` button and performs the `pong` transaction, we have to invalidate the cache and we will use the transaction processor to identify all the `pong` transactions on the blockchain that have the receiver set to our smart contract address. From 251b53657aa5cb6fc0182a2473a9ab579f9a1f1e Mon Sep 17 00:00:00 2001 From: danielailie Date: Thu, 20 Mar 2025 13:42:02 +0200 Subject: [PATCH 9/9] Fix redirect to v14 --- docusaurus.config.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index f40159de9..e013ae8c6 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -5,8 +5,8 @@ // See: https://docusaurus.io/docs/api/docusaurus-config import { themes as prismThemes } from "prism-react-renderer"; -import remarkMath from "remark-math"; import rehypeKatex from "rehype-katex"; +import remarkMath from "remark-math"; /** @type {import('@docusaurus/types').Config} */ const config = { @@ -432,7 +432,7 @@ const config = { }, { from: "/sdk-and-tools/sdk-js/sdk-js-cookbook", - to: "/sdk-and-tools/sdk-js/sdk-js-cookbook-v13", + to: "/sdk-and-tools/sdk-js/sdk-js-cookbook-v14", }, { from: "/sdk-and-tools/erdjs/writing-and-testing-erdjs-interactions",