Photo by Markus Spiske on Unsplash
Building a Resource Sharing dApp on NEAR Protocol with Rust
Learn how to build a complete dApp on the NEAR protocol
Table of contents
- Introduction
- Prerequisites
- Overview
- Setting up our project
- Building our contract
- Add a resource
- List all resources
- Voting a resource
- Donate to resource creator
- Get total Resources and donations count
- Deploying the smart contract🚀
- Interact with our contract by calling contract methods
- Vote on a resource
- Donate to a Resource
- Interact with our contract by calling contract methods
- Vote on a resource
- Donate to a Resource
- List all resources created
- Building our frontend
- Conclusion
- References
Introduction
In this tutorial, we will build a resource-sharing dApp on the NEAR blockchain. Our smart contract will be written in rust and our frontend will be written in React. In the dApp we built, the user can login and post/create their own resources, view the list of resources posted by other users, donate to the resource creator, and vote on a particular resource they find useful.
After we have implemented all the features in our smart contract, we will connect the smart contract to our react frontend which looks like this
Prerequisites
To follow along and complete this tutorial, you will need the following:
- The near-cli
- A NEAR account
- Little knowledge on React.
- Install the rust toolchain.
- npx binary.
- Git and Node.js(v14+) installed
As I mentioned earlier in the introduction, the smart contract for this dApp is written in Rust. If you are not familiar with Rust, you can take this 30 minutes crash course.
Make sure you have everything installed from the prerequisites including
near-cli
and creating a NEAR account
Overview
We have looked at the things we need to install to follow along in this tutorial, now let’s look at the things we will be doing as the tutorial progresses. We will use the npx binary to scaffold our project with create-near-app which provides us with options of what to use for the frontend and backend. After scaffolding our project we will look at the file structure, write our smart contract and deploy and test that all functions are working and connect to our React frontend.
Setting up our project
We setup our project with create-near-app by running the following command:
npx create-near-app --frontend=react --contract=rust resourcedapp cd resourcedapp
with the --frontend=react
and --contract=rust
options, we create our project to have a react frontend and a rust contract. After waiting a while for the command to install everything, we see this in our terminal:
Now everything is successfully installed, and everything is ready to go, let’s look at what our file structure looks like.
File structure
In our file structure, we have the contract
folder, where we will be writing our contract specifically in the src
directory. Then there is our near-dev
folder that contains the configuration for deploying our contract in development mode and our src
folder for our frontend. Looking closer into our contract folder:
In the contract folder we have the src
folder where we’ll be creating a model.rs
, lib.rs
and utils.rs
file. We already have lib.rs
file, so no need to create another one. The cargo.toml
file contains information about our contract, including all the dependencies we need.
Building our contract
In the last section we have seen how to set up our project and how our project is structured. Now let’s dive into building our contract.
How does our contract work
Before building our contract, let’s discuss how our contract works, and the functions/endpoints we will call. In our contract, we want to be able to:
- Create a resource- To do this we will need a
add_resource
function, that when invoked will create a new resource and add it to our array of resources. - List all our resources - To do this, we will need a function
list_resources
to list all our resources. - Vote on a resource - To do this, we will need a
add_vote
function to add a vote to a resource. - Donate to resource creator - To do this, we need an
add_donation
function that donates a certain amount of the NEAR token to the resource creator.
Create a model.rs
, lib.rs
and utils.rs
file in the src
folder. In our utils
file we copy and paste the following code.
The
utils.rs
file contains some helpful functions and types we will be using as we build our contract, with some comments explaining what they do. Don’t get intimidated as you only need to copy and paste it into yourutils.rs
file
// utils.rs use near_sdk::{ env, PromiseResult, }; /// == TYPES ==================================================================== /// Account Ids in Near are just strings. pub type AccountId = String; /// Gas is u64 pub type Gas = u64; /// Amounts, Balances, and Money in NEAR are u128. pub type Amount = u128; pub type Balance = Amount; pub type Money = Amount; /// Timestamp in NEAR is a number. pub type Timestamp = u64; /// /// == CONSTANTS ================================================================ /// /// TODO: revist MIN_ACCOUNT_BALANCE after some real data is included b/c this /// could end up being much higher /// ONE_NEAR = unit of NEAR token in yocto Ⓝ (1e24) pub const ONE_NEAR: u128 = 1_000_000_000_000_000_000_000_000 as u128; /// XCC_GAS = gas for cross-contract calls, ~5 Tgas (teragas = 1e12) per "hop" pub const XCC_GAS: Gas = 20_000_000_000_000; /// MIN_ACCOUNT_BALANCE = 3 NEAR min to keep account alive via storage staking pub const MIN_ACCOUNT_BALANCE: u128 = ONE_NEAR 3; /// == FUNCTIONS ================================================================ /// Converts Yocto Ⓝ token quantity into NEAR, as a String pub fn asNEAR(amount: u128) -> String { format!("{}", amount / ONE_NEAR) } /// Converts a quantity in NEAR into Yocto Ⓝ tokens pub fn toYocto>(amount: D) -> u128 { ONE_NEAR amount.into() } /// Asserts that the contract has called itself pub fn assert_self() { let caller = env::predecessor_account_id(); let current = env::current_account_id(); assert_eq!(caller, current, "Only this contract may call itself"); } /// Asserts that only a single promise was received, and successful pub fn assert_single_promise_success(){ assert_eq!( env::promise_results_count(), 1, "Expected exactly one promise result", ); match env::promiseresult(0) { PromiseResult::Successful() => return, _ => panic!("Expected PromiseStatus to be successful"), }; }
Create our models
We can describe the model in our contract as a custom data container for defining new types. Just like the way we have primitive types, we can create our own custom types in our model that describe or are patterned to what we are building. The code for our model.rs
file can be found below and we will explain what it does step by step
model.rs use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
#[allow(unused_imports)] use near_sdk::{env, near_bindgen}; use near_sdk::serde::{Deserialize, Serialize};
use crate::utils::{ AccountId, Money, Timestamp };
#[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
#[serde(crate = "near_sdk::serde")] pub struct Resource{ id: i32, pub creator: AccountId, created_at: Timestamp, title: String, pub url: String, pub total_donations: u128, pub total_votes: i64, description: String, pub votes: Vec }
impl Resource{ pub fn new(id:i32, title: String, url:String, description: String) -> Self {
Resource{ id, creator: env::signer_account_id(), created_at: env::block_timestamp(), title, url, total_donations: 0, total_votes : 0, description, votes: vec![], } } }
#[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
#[serde(crate = "near_sdk::serde")]
pub struct Donation {
amount: Money,
donor: AccountId,
}
impl Donation {
pub fn new() -> Self {
Donation{
amount: env::attached_deposit(),
donor: env::predecessor_account_id(),
}
}
}
Let’s breakdown what the code above is doing:
use near_sdk::borsh::{self, BorshDeserialize, BorshSerialize};
#[allow(unused_imports)] use near_sdk::{env, near_bindgen}; use near_sdk::serde::{Deserialize, Serialize};
Above we have the rust contract standard imports and can see the dependencies that are used in the Cargo.toml
file. The dependencies that have to do with serialization which are BorshDeserialize
and BorshSerialize
are used to bundle our contract code and storage so it is ready for NEAR blockchain. The use
statement before the dependencies just shows which dependencies we are using in this file.
env
is used to log into your console or return some useful information like the signer’s account
// model.rs use crate::utils::{ AccountId, Money, Timestamp };
Here, we just import some custom types we will be using from our utils.rs
file. After this piece of code, we have our Resource struct
which is crucial to our contract.
Resource Struct
A struct
in rust is similar to classes in other languages. Structs kind of hold the state of the contract. Building our contract, our struct
will be followed by an impl
, where we write the core logic function of the contract.
// model.rs
#[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
#[serde(crate = "near_sdk::serde")] pub struct Resource{ id: i32, pub creator: AccountId, created_at: Timestamp, title: String, pub url: String, pub total_donations: u128, pub total_votes: i64, description: String, pub votes: Vec }
We want to be able to add new resources, so the Resource struct
above contains the fields of information a particular resource project will have. We can see that each of the fields has a type. The last field is a type of vector, but we can just look at it as an array.
The keyword
pub
means that whatever we prefix it with is public and can be used externally
impl Resource{ pub fn new(id:i32, title: String, url:String, description: String) -> Self {
Resource{ id, creator: env::signer_account_id(), created_at: env::block_timestamp(), title, url, total_donations: 0, total_votes : 0, description, votes: vec![], } } }
The block of code above is where we initialize our Resource
model. The contract was initialized with some arguments and also contains some default data like the creator
field which has its default as the signer account’s id. The total_donations
and total_votes
are also set to zero by default. The contract is also initialized with some arguments that we will provide when invoking the function to add a resource. The arguments are:
id
: A unique ID for each resource.title
: The title of our resource.url
: a url that links to the resource.description
: A little description of our resource.
The other model we have in our model.rs
file is:
#[derive(Clone, Serialize, Deserialize, BorshDeserialize, BorshSerialize)]
#[serde(crate = "near_sdk::serde")]
pub struct Donation {
donor: AccountId,
}
impl Donation {
pub fn new() -> Self {
Donation{
donor: env::predecessor_account_id(),
}
}
}
For our donation struct
we have a donor
field we would like to store on the blockchain. Then we initialize the donation struct
with the donor
with the default value of the predecessor_account_id()
which signifies the person that donated to the resource creator.
Now let's move over to our lib.rs
file where we call the different functions that will make all the features for our resource sharing dApp possible.
Add a resource
We have created our models in the model.rs
file and we are now moving over to our lib.rs
file where we will be writing the different functions we need to call to perform certain operations in the dApp. Before we get into the nitty-gritty of things, let's look at some code we need available at the top of our lib.rs
file.
lib.rs mod models; mod utils; use crate::{ utils::{ AccountId, ONE_NEAR, }, models::{ Resource, Donation } }; // To conserve gas, efficient serialization is achieved through Borsh (borsh.io) use near_sdk::{borsh::{self, BorshDeserialize, BorshSerialize}, Promise};
#[allow(unused_imports)] use near_sdk::{env, PromiseIndex, near_bindgen}; near_sdk::setup_alloc!();
The first thing we do in our lib.rs
file is import the utils.rs
and models.rs
modules which are also in the same src
folder. After that, we signify what in particular we will be using in those modules with the use
keyword, then finally the rust standard imports. Now to continue we need to create a contract struct
#[near_bindgen]
#[derive(Clone, Default, BorshDeserialize, BorshSerialize)] pub struct Contract { owner: AccountId, resources: Vec, donations: Vec, }
The
#[near_bindgen]
before our contractstruct
is to allow easy compilation of our contract code into WebAssembly, so it is compatible and optimizable for the NEAR blockchain
In our contract struct
we have the the owner
field and then we have the resources
and donation
field which are just vectors of their models Resources
and Donation
. Next, we will have the impl
#[near_bindgen] impl Contract{
#[init] pub fn init( owner: AccountId, ) -> Self{ let resources: Vec = Vec::new(); let donations: Vec = Vec::new();
Contract{ owner, resources, donations } } }
In our Contract impl
the #init
allows us to create a custom initialization of the contract with a function that takes in a parameter of owner
. We construct a new, empty vector with the Vec::new()
and assign it to resources
and donations
respectively.
The vector will not be allocated until elements are pushed onto it.
The add_resources
function
After our initialization, we start writing our functions. The first function we have is the add_resources
function.
pub fn add_resources(&mut self, title: String, url:String, description: String) {
let id = self.resources.len() as i32;
self.resources.push(Resource::new( id, title, url, description )); env::log("Added a new Resource".as_bytes()); }
The add_resources
function accepts three parameters, title
, url
, and description
we will pass in when the function is called. We want to have a unique id
for each resource created, so we create an id
variable for the length of the total resources we have in our vector. We have added a new resource to our list of resources. So each time we call the add_resources
function, we update the blockchain state and a new resource is added to the list with the information we have provided as parameters. Finally, we log “Added a new resource” to the blockchain.
Notice how we also passed in
&mut self
in our function?. It simply means we are passing on a mutable reference ofself
which points to ourContract
List all resources
We need to return all resources, so we create a function list_resources
.
pub fn list_resources(&self) -> Vec { let resources = &self.resources; return resources.to_vec(); }
When we call the function, we return our resources as a vector that displays an array of all the resources created.
Voting a resource
Assuming there is a resource the user likes, the user can vote on that resource. So in our contract, we added an add_vote
function.
pub fn add_vote(&mut self, id:usize){ let resource: &mut Resource = self.resources.get_mut(id).unwrap(); let voter = env::predecessor_account_id(); resource.total_votes = resource.total_votes + 1; env::log("vote submitted succesfully".as_bytes()); resource.votes.push(voter);
}
We pass an id
parameter to the add_vote
function to determine the particular project we want to vote on. Then we can access the project by calling .get_mut(id)
and passing in the id of the resource. We set our voter to be the predecessor_account_id()
and then we mutate our state with this line.
resource.total_votes = resource.total_votes + 1;
This increments the total_votes
field by 1, each time we call the function, then we add the voter to the votes vector, and we can see who has voted on that project.
Donate to resource creator
To donate to a particular resource, we create an add_donation
function and pass in the id
of the resource we want to donate to, and the amount of NEAR token we want to donate.
pub fn add_donation(&mut self, id:usize, amount:u128) { let transfer_amount: u128 = 1_000_000_000_000_000_000_000_000 * amount; let resource: &mut Resource = self.resources.get_mut(id).unwrap(); resource.total_donations = resource.total_donations + transfer_amount; self.donations.push(Donation::new());
Promise::new(env::predecessor_account_id()).transfer(transfer_amount); env::log("You have donated succesfully".as_bytes()); }
Here we pass as a parameter, the id
of the resource and the amount
we want to donate. We then add the donated amount to our current total_donations
balance, and push a new donation each time we call the function and the amount in NEAR is transferred to the owner of the created resource.
Get total Resources and donations count
Let’s add two more functions to our contract to determine the total resources we have and the total sum of donations a particular resource creator has received.
pub fn resource_count(&mut self) -> usize { return self.resources.len(); }
pub fn get_donation_count(&mut self, id:usize) -> u128 { let resource: &mut Resource = self.resources.get_mut(id).unwrap(); return resource.total_donations; }
We have created two functions resource_count
that simply returns the length of our resource vector and get_donation_count
to return the total donations a particular resource creator has received.
Now that we are done with all the functions for this contract, it is time to deploy and invoke the functions in our contract to make sure everything is working fine, and that is what we will be doing in the next section.
Deploying the smart contract🚀
Before we deploy our smart contract, let’s first login with the near-cli
into the NEAR account we created.
Login with **near-cli**
Make sure you already have the near-cli
installed, you can see the installation steps from the link in the prerequisites. Then run this command:
near login
After login to the account we created, we see a successful message in the terminal.
Deploy the contract
To deploy our resource sharing contract, we need to create an account for it. Since we have already created a testnet account, in my case umamad.testnet
, we can create a subaccount under umamad.testnet
to deploy the contract.
near create-account resourcedapp.umamad.testnet --masterAccount umamad.testnet
In your case, the
umamad
will be replaced by your own NEAR testnet account name you created
Once your sub-account is created, navigate to src/config.js
and modify the line that sets the account name of the contract. Set it to the sub-account id
you created above.
const CONTRACT_NAME = process.env.CONTRACT_NAME || 'resourcedapp.umamad.testnet'
we can finally deploy our contract. Since we use create-near-app, deploying our contract is easy and can be done with the command
near deploy
we can see that after running the command, our contract was deployed successfully. yay!.
If you make a change in your contract, you can just delete the sub account with the command
near delete resourcedapp.umamad.testnet umamad.testnet
, and create it again.
Interact with our contract by calling contract methods
Now we can interact with our contract and make sure all our functions are doing what they are supposed to do. Let’s test out each function!.
Add a resource
Call add_resources
with the near-cli
:
near call resourcedapp.umamad.testnet add_resources '{"title": "Youtube Music", "url": "music.youtube.com", "description":"listen to music"}' --accountId umamad.testnet
What the command above does is to call the contract deployed on resourcedapp.umamad.testnet
. On the contract, there is a method called add_resources
with three arguments provided and is signed by umamad.testnet
.
In your terminal, you will see an output like this:
Vote on a resource
Call add_vote
with the near-cli
:
near call resourcedapp.umamad.testnet add_vote '{"id":0}' --accountId umamad.testnet
this command increments the vote of the resource selected by one.
Donate to a Resource
Call the add_donate
method with the near-cli
near call resourcedapp.umamad.testnet add_donation '{"id":0, "amount":1}' --accountId umamad.testnet
this command calls the add_donation
method and 1 NEAR is sent to the resource creator.
Interact with our contract by calling contract methods
Now we can interact with our contract and make sure all our functions are doing what they are supposed to do. Let’s test out each function!.
Add Resources
Call add_resources
with the near-cli
:
near call resourcedapp.umamad.testnet add_resources '{"title": "freeCodeCamp", "url": "freecodecamp.com", "description":"learn how to code from this site"}' --accountId umamad.testnet
What the command above does is to call the contract deployed on resourcedapp.umamad.testnet
. On the contract, there is a method called add_resources
with three arguments provided and is signed by umamad.testnet
.
In your terminal, you will see an output like this:
Vote on a resource
Call add_vote
with the near-cli
:
near call resourcedapp.umamad.testnet add_vote '{"id":0}' --accountId umamad.testnet
this command increments the vote of the first resource created by one.
Donate to a Resource
Call the add_donate
method with the near-cli
near call resourcedapp.umamad.testnet add_donation '{"id":0, "amount":1}' --accountId umamad.testnet
this command calls the add_donation
method and 1 NEAR is sent to the resource created.
List all resources created
Call the list_resources
method.
near call resourcedapp.umamad.testnet list_resources --accountId umamad.testnet
This command lists all the resources created to the terminal.
You can see the two resources created from the image and all the information we set. We have invoked the methods to see how they are working, let’s now build the react frontend
Building our frontend
We are finally done building the smart contract. To connect and build our React frontend, we will be working on the src
folder.
First, navigate to utils.js
and update your change and view methods
window.contract = await new Contract(window.walletConnection.account(), nearConfig.contractName, { // View methods are read only. They don't modify the state, but usually return some value. viewMethods: ['get_donation_count',"resource_count"], // Change methods can modify the state. But you don't receive the returned value when called. changeMethods: ['add_resources', 'add_vote','add_donation',"list_resources"], }) }
Take a look at the code in our App.js
:
import 'regenerator-runtime/runtime' import { useEffect, useState } from 'react' import ListResources from './components/ListResources' import CreateResource from './components/CreateResource' import React from 'react' import { login, logout } from './utils' import './global.css' import getConfig from './config' const { networkId } = getConfig(process.env.NODE_ENV || 'development') export default function App() { const [resources, setResources] = useState([]) const [toggleModal, setToggleModal] = useState(false) function addResource() { setToggleModal(!toggleModal) } useEffect( () => { // in this case, we only care to query the contract when signed in if (window.walletConnection.isSignedIn()) { // window.contract is set by initContract in index.js window.contract.list_resources().then((resources) => { const resourceList = [...resources] setResources(resourceList) console.log(resourceList); }) } }, [], ) // if not signed in, return early with sign-in prompt if (!window.walletConnection.isSignedIn()) { return (
Welcome to resource sharing dApp
Click the button below to sign in:
Sign in
In the App.js
file above, we import the configurations, utils, and components we will be using. We created our resources
state which is an array of objects containing all our resources. In the useEffect
hook, we call the list_resources
method from window.contract
which has been set by initContract
in the index.js
file. We then set the resources
state, using React’s setState
.
Next, we check if a user is signed in, and if no user is signed in, we prompt them to.
Once signed in, we will see available resources we can donate to.
List Resources component
In our App.js
component, we looped through all our resources created and passed in the object prop to our ListResources
component. This is what our ListResources
component looks like.
import React, { useState } from 'react' const ONE_NEAR = 1_000_000_000_000_000_000_000_000 function ListResources({ resource }) { console.log(resource); const [donationAmount, setDonationAmount] = useState(0) const [showDonateNotification, setShowDonateNotification] = useState(false) function donate(e) { e.preventDefault() console.log(donationAmount) window.contract.add_donation({ id: resource.id, amount: donationAmount * 1 }) setShowDonateNotification(!showDonateNotification) } return (
{resource.title}
{' '} {resource.creator}description:
{resource.description}
Link:{resource.url}
Votes: {resource.total_votes}
{ window.contract.add_vote({ id: resource.id }) }} > Votetotal donations: {resource.total_donations / ONE_NEAR} NEAR
setDonationAmount(e.target.value)}{showDonateNotification && }Donate
In the ListResources
component, we set the donation amount state and when we want to donate, by clicking on the Donate
button, the donate
function is called which calls the add_donation
method in our contract with the argument we provide from the form.
We also have a notification component that notifies the user when our donation is successful.
Create Resource Component
Our CreateResource component is a modal that contains a form for us to add a new resource.
import React, { useState } from 'react'
function CreateResource({toggleModal}) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [url, setUrl] = useState('')
const [showNotification, setShowNotification] = useState(false)
const handleSubmit = (event) => {
event.preventDefault()
window.contract.add_resources({title:title, url:url, description:description})
setShowNotification(!showNotification)
alert(resource info: ${title} ${url} ${description}
)
}
console.log(its ${toggleModal}
);
return (
{showNotification && }
) } function Notification() { return (
) } export default CreateResourceThis component conditionally renders when the toggleModal
state is true
. We have a state for the resource title
, description
, and url
, and each time our input changes, we set the state to the input value. On submitting the form, the handleSubmit
function runs and our add_resource
method is called from the window.contract
with the arguments we got from the input values.
Yay!. We have concluded the tutorial and you can find the source code here.
Conclusion
Finally!, we have come to the end of this tutorial. We learned how to build a resource sharing dApp on the NEAR blockchain using Rust for the backend and React for the frontend, test our methods and deploy our smart contract live.
References
- NEAR docs.