Building a Resource Sharing dApp on NEAR Protocol with Rust

Learn how to build a complete dApp on the NEAR protocol

·

19 min read

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:

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:

  1. 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.
  2. List all our resources - To do this, we will need a function list_resources to list all our resources.
  3. Vote on a resource - To do this, we will need a add_vote function to add a vote to a resource.
  4. 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 your utils.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:

model.rs

#[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 contract struct 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 of self which points to our Contract

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

) } return ( // use React Fragment, <>, to avoid wrapping elements in unnecessary divs <>
Sign out {window.accountId}
Add a resource
{resources.map((resource, id) => { return (
) })}
</> ) }

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 }) }} > Vote

total donations: {resource.total_donations / ONE_NEAR} NEAR

setDonationAmount(e.target.value)}

Donate

{showDonateNotification && }
) } function DonateNotification() { return ( ) } export default ListResources

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 (

{toggleModal == true && (
Enter resource title: setTitle(e.target.value)} /> Enter resource url: setUrl(e.target.value)} /> Enter resource description: setDescription(e.target.value)} />
)}

{showNotification && }

) } function Notification() { return (

) } export default CreateResource

This 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