NEAR Tutorial - Build a Decentralized Blog

·

15 min read

NEAR Tutorial - Build a Decentralized Blog

Introduction

NEAR protocol is a layer 1 sharded proof of stake blockchain focused on scalability. It is both user and developer-friendly, smart contracts can be built on NEAR with either AssemblyScript or Rust.

Learn more about the NEAR Protocol.

In this article, we'll build a full-stack decentralized blog on the NEAR blockchain using Reactjs, Chakra UI, AssemblyScript, near-sdk-as, and near-api-js.

Prerequisites

  • Have a NEAR Testnet account, visit NEAR wallet to create one.

  • Javascript/Typescript knowledge.

  • Familiarity with React.

  • Ensure that Node.js version >= 12.x.x and npm are installed on your system.

NEAR Blog

We'll create and deploy a decentralized blog dapp where users can read blog posts, connect their NEAR wallet, publish blog posts and also appreciate other authors by sending NEAR tokens.

The smart contract will be built using AssemblyScript and near-sdk-as library. This library is a collection of tools for building NEAR smart contracts written in AssemblyScript.

Project Setup and Installation

To start building NEAR smart contracts seamlessly, you need to have some important tools installed globally. Run each of these commands:

npm install -g near-cli
npm install -g assemblyscript
npm install -g asbuild

Login to NEAR

For this next step, you need to have a NEAR Testnet account.

To login to your Testnet account, run the code below in your terminal:

near login

This will lead you to your browser to authorize.

Screenshot 2022-08-17 at 11.34.01 AM.png

Click on the 'Next' button then click on 'Connect'.

Smart Contract

In this section, the smart contract will be built with AssemblyScript and near-sdk-as.

Have no prior experience with AssemblyScript?

Don't fret, AssemblyScript is very similar to TypeScript, so getting started with AssemblyScript is not much of a hassle.

Project Setup

  • Create a new root directory for the project, you can call it near-blog-contract.

  • In the root directory create a new file asconfig.json. Add the code block below in the file:

      {
          "extends": "near-sdk-as/asconfig.json"
      }
    
  • Create an assembly directory, and add a tsconfig.json file in it. Add the code block below in the assembly/tsconfig.json file:

      {
        "extends": "../node_modules/assemblyscript/std/assembly.json",
        "include": [
          "./**/*.ts"
        ]
      }
    
  • Create a new file assembly/as_types.d.ts in the assembly directory. Add the code below:

      /// <reference types="near-sdk-as/assembly/as_types" />
    
  • Run the following commands to initialize the project in the project root directory and install dependencies:

      npm init
      npm install -D near-sdk-as
    
  • Create an entry file index.ts in the assembly directory.

Your near-blog-contract structure should look like this:

├── asconfig.json
├── assembly
│   ├── as_types.d.ts
│   ├── index.ts
│   └── tsconfig.json
├── package.json
└── yarn.lock

Creating the Blog Model

Create a new file model.ts under assembly/ directory.

The model of a single blog post will be defined in this file, a model is an AssemblyScript class that defines a new type, in our case the Blog type.

Paste the code below in assembly/model.ts to create the Blog model:

import { u128, context, PersistentUnorderedMap } from "near-sdk-as";

// @nearBindgen is used to serialize the `Blog` class before storing it on the blockchain.
@nearBindgen
export class Blog {
  // Define the `Blog` fields and data types
  id: string;
  title: string;
  content: string;
  author: string;
  createdAt: u64;
  appreciationCost: u128; // readers will be able to appreciate an author by sending NEAR tokens
  appreciationCount: i32; // the number of readers that appreciated a blog post

  // Define methods for the `Blog`
  // 1) `createBlog` method takes in a payload and returns a new `Blog` object
  public static createBlog(payload: Blog): Blog {
    const blog = new Blog();
    blog.id = payload.id;
    blog.title = payload.title;
    blog.content = payload.content;
    blog.appreciationCost = payload.appreciationCost;
    blog.author = context.sender;
    blog.createdAt = context.blockTimestamp;

    return blog;
  }

  // 2) `incrementAppreciationCount` method increases the `appreciationCount` of a blog
  public incrementAppreciationCount(): void {
    this.appreciationCount = this.appreciationCount + 1;
}
}

export const blogPosts = new PersistentUnorderedMap<string, Blog>("BLOG_POSTS");

We have 3 imports from near-sdk-as.

  1. u128: The 128 bit unsigned integer makes it possible to store the appreciationCost in Yocto, which is the smallest unit of NEAR.

  2. context: This is an object containing information about the transaction.

  3. PersistentUnorderedMap: This allows us to create a new persistent unordered map blogPosts (a collection of all blog posts).

Writing the Smart Contract

Navigate to assembly/index.ts, this is where the smart contract will be written.

Our contract will store and fetch data from the blockchain.

Replace the entire code in assembly/index.ts with:

import { Blog, blogPosts } from "./model";
import { context, ContractPromiseBatch } from "near-sdk-as";

export function setBlog(blog: Blog): string {
  let storedBlog = blogPosts.get(blog.id);
  // check if blog with same id already exist
  if (storedBlog !== null) {
    throw new Error(`A blog post with ${blog.id} already exists`);
  }
  blogPosts.set(blog.id, Blog.createBlog(blog)); // create a new blog using `createBlog` method
  return "Blog Post Created!";
}

This function creates a new blog.

The next functions will retrieve a single blog post and all blog posts respectively.

Add the new functions below to assembly/index.ts:

export function getBlog(id: string): Blog | null {
  // assert that blog with given id exists
  assert(blogPosts.contains(id), "This Blog doesn't exist");
  return blogPosts.get(id);
}

export function getBlogs(): Blog[] {
  return blogPosts.values();
}

In getBlog function, the assert method from AssemblyScript is used to verify that a blog with the given id exists. If it exists, the blog is returned.

getBlogs() returns an array of all blog posts.

Now let's create a function that enables a user to appreciate an author. Add the code block below to assembly/index.ts:

export function appreciateBlog(blogId: string): void {
  const blog = getBlog(blogId); // retrieve blog
  if (blog == null) {
    throw new Error("Blog post not found"); // check if blog exists
  }
  // assert that the reader sends the correct appreciation cost
  assert(blog.appreciationCost.toString() == context.attachedDeposit.toString(), "Attached deposit should equal to the appreciation cost");

  ContractPromiseBatch.create(blog.author).transfer(context.attachedDeposit); // send `appreciationCost` to blog author
  blog.incrementAppreciationCount(); // increment `appreciationCount`
  blogPosts.set(blog.id, blog); // update blog
}

ContractPromiseBatch is used to initialize the transfer of NEAR tokens from one wallet to another within an AssemblyScript contract.

It takes in the receiver in its .create() and the amount in .transfer().

That's it! We're done with writing the smart contract 🚀

Compile and Deploy Contract

Let's deploy to NEAR Testnet.

Compile and deploy the contract to wasm by running:

npm run deploy

Screenshot 2022-08-19 at 1.39.40 PM.png

Done deploying to dev-1660XXXXXXXX, this is your contract's address.

Search your transaction id at explorer.testnet.near.org/transactions to view the deployed contract.

Screenshot 2022-08-25 at 12.52.12 PM.png

Contract Calls

In this section, we will call our smart contract's functions.

The two kinds of function calls are view and change.

  • view calls read data from the blockchain.

  • change calls write to the blockchain and change its state.

In our smart contract, setBlog and appreciateBlog are change calls because they write data to the blockchain and update it.

While getBlog and getBlogs are view calls because they only read data from the blockchain.

Change Contract Calls

The syntax for change contract calls:

near call <CONTRACT-ADDRESS> <METHOD-NAME> <PAYLOAD> --accountId=<YOUR-ADDRESS>
  • <CONTRACT-ADDRESS>: The address your contract was deployed to. Navigate to neardev/dev-account to get it.

  • <METHOD-NAME>: The name of the function you're calling.

  • <PAYLOAD>: The payload of the function.

  • <YOUR-ADDRESS>: The NEAR account making the call.

setBlog call:

near call <CONTRACT-ADDRESS> setBlog '{"blog": {"id": "00-Blog", "title": "First Blog Post", "content": "Article body", "appreciationCost": "1000000000000000000000000" }}' --accountId=<YOUR-ADDRESS>

Replace <CONTRACT-ADDRESS> with your contract's address.

Navigate to neardev/dev-account to get your contract's address.

Replace <YOUR-ADDRESS> with your Testnet account, for example 'giftea.testnet'

You should see an output similar to this if successful.

Screenshot 2022-08-19 at 1.44.46 PM.png

To call the appreciateBlog, we need to create another testnet account to act as the reader who intends to appreciate an author. With NEAR, you can create sub-accounts, so we'll do that.

Syntax:

near create-account <SUB_ACCOUNT>.<MY_ACCOUNT> --masterAccount <MY_ACCOUNT> --initialBalance 5

Example:

near create-account me.giftea.testnet --masterAccount giftea.testnet --initialBalance 5

Replace giftea.testnet with your testnet account.

Screenshot 2022-08-19 at 1.52.35 PM.png

To appreciate the author of the newly created blog, run the code below:

near call <ADD_YOUR_CONTRACT_ADDRESS_HERE> appreciateBlog '{"blogId": "00-Blog"}' --depositYocto=1000000000000000000000000 --accountId=<ADD_YOUR_SUB_ACCOUNT>

You should see an output similar to this if successful.

Screenshot 2022-08-19 at 2.07.19 PM.png

View Contract Calls

The syntax for view contract calls:

near view <CONTRACT-ADDRESS> <METHOD-NAME> <PAYLOAD>

Let's call the getBlog function:

near view <ADD_YOUR_CONTRACT_ADDRESS_HERE> getBlog '{"id": "00-Blog"}'

You should see an output similar to this if successful.

Screenshot 2022-08-19 at 2.17.21 PM.png

We are done with the smart contract for our project.

Awesome!

Building Frontend React Application

Project Set-Up

  1. Start a new React application and install dependencies.
npx create-next-app@latest
yarn add @chakra-ui/react @emotion/react @emotion/styled framer-motion uuid near-api-js react-router-dom buffer regenerator-runtime
  1. Create a new file near-config.js in the root directory. Add the code below:

     const CONTRACT_NAME = process.env.CONTRACT_NAME || "${CONTRACT_NAME}"; // Your Contract address; dev-xxxxxxxxxxxx-xxxx 
    
     function getConfig(env) {
       switch (env) {
         case "mainnet": 
           return {
             networkId: "mainnet",
             nodeUrl: "https://rpc.mainnet.near.org",
             contractName: CONTRACT_NAME,
             walletUrl: "https://wallet.near.org",
             helperUrl: "https://helper.mainnet.near.org",
             explorerUrl: "https://explorer.mainnet.near.org",
           };
         case "testnet":
           return {
             networkId: "testnet",
             nodeUrl: "https://rpc.testnet.near.org",
             contractName: CONTRACT_NAME,
             walletUrl: "https://wallet.testnet.near.org",
             helperUrl: "https://helper.testnet.near.org",
             explorerUrl: "https://explorer.testnet.near.org",
           };
         default:
           throw Error(`Unknown environment '${env}'.`);
       }
     }
    
     export default getConfig;
    
  2. Create a new file near-api.js in the root directory and add the code below:

     import { connect, Contract, keyStores, WalletConnection } from 'near-api-js';
     import getConfig from "./near-config";
    
     const nearConfig = getConfig("testnet");
    
     // Initialize contract & set global variables
     export async function initContract() {
       // Initialize connection to the NEAR testnet
       const near = await connect(
         Object.assign(
           { keyStore: new keyStores.BrowserLocalStorageKeyStore() },
           nearConfig
         )
       );
    
       // Initializing Wallet based Account. It can work with NEAR testnet wallet that
       // is hosted at https://wallet.testnet.near.org
       window.walletConnection = new WalletConnection(near);
    
       // Getting the Account ID. If still unauthorized, it's just empty string
       window.accountId = window.walletConnection.getAccountId();
    
       // Initializing our contract APIs by contract name and configuration
       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: ["getBlog", "getBlogs"],
         // Change methods can modify the state. But you don't receive the returned value when called.
         changeMethods: ["setBlog", "appreciateBlog"],
       });
     }
    
     export function signOutNearWallet() {
       window.walletConnection.signOut();
       // reload page
       window.location.reload();
     }
    
     export function signInWithNearWallet() {
       // Allow the current app to make calls to the specified contract on the
       // user's behalf.
       // This works by creating a new access key for the user's account and storing
       // the private key in localStorage.
       return window.walletConnection.requestSignIn({
         contractId: env.contractName,
       });
     }
    

Initialize Contract Calls

We don't need an ABI for NEAR development as we do for Ethereum contracts. Instead, we create functions that interact with the NEAR wallet and smart contract.

The functions located in near-api.js are functions to interact with the NEAR wallet.

Now let's create functions that will interact with the blockchain.

Create a new file near-blog-api.js, and paste the code below:

import { v4 as uuid4 } from "uuid";
import { parseNearAmount } from "near-api-js/lib/utils/format";

const GAS = 100000000000000; // gas fee

export async function createBlog(blog) {
  blog.id = uuid4(); // creates unique id
  blog.appreciationCost = parseNearAmount(blog.appreciationCost + ''); // parseNearAmount converts price to correct format
  return window.contract.setBlog({ blog }, GAS, parseNearAmount(0.52 + ""));
}

export function getBlogs() {
  return window.contract.getBlogs();
}

export function getBlog(id) {
  return window.contract.getBlog(id);
}

export async function appreciateBlog(id, appreciationCost) {
  return window.contract.appreciateBlog({ blogId: id }, GAS, appreciationCost);
}

With window.contract, we have access to all view and change methods we specified in near-api.js.

Building Components

Create a new folder components within the src directory.

Nav Component

Create a new file components/Nav.jsx, and paste the code below:

import React from "react";
import { Flex, Spacer, Box, Button } from "@chakra-ui/react";
import { signInWithNearWallet, signOutNearWallet } from "../near-api";
import { Link, useNavigate } from "react-router-dom";

const Nav = ({ accountId }) => {
  let navigate = useNavigate();
  return (
    <Flex bg="blue.800" alignItems="center">
      <Box p="4" color="gray.50">
        <Link to="/">NEAR Blog</Link>
      </Box>
      <Spacer />
      <Box p="4">
        <Button onClick={() => navigate("/add")}>Add Post</Button>
      </Box>
      <Box p="4">
        {accountId ? (
          <Button style={{ float: "right" }} variant="link" onClick={signOutNearWallet}>
            Sign out {accountId}
          </Button>
        ) : (
          <Button style={{ float: "right" }}  colorScheme='teal' onClick={signInWithNearWallet}>
            Connect
          </Button>
        )}
      </Box>
    </Flex>
  );
};

export default Nav;

Layout Component

Create a new file components/Layout.jsx, and add the code below:

import { ChakraProvider, Container } from "@chakra-ui/react";
import Nav from "./Nav";

export const Layout = ({ children }) => {
    return (
      <ChakraProvider>
        <Nav accountId={window.accountId} />
        <Container>{children}</Container>
      </ChakraProvider>
    );
  };

Update the entire code in App.js with the code block below:

import "regenerator-runtime/runtime";
import React from "react";
import "./assets/global.css";
import { BrowserRouter } from "react-router-dom";
import { Routes, Route } from "react-router-dom";

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
          {/* Routes will go here */}
      </Routes>
    </BrowserRouter>
  );
}

Blog Card Component

Create a new file components/BlogCard.jsx, and add the code:

import { Box, Image } from "@chakra-ui/react";
import { Link } from "react-router-dom";

function Card({ blog }) {
  const img = "https://i.imgur.com/Jo1AKdb.png";

  return (
    <Link to={`/blog/${blog?.id}`}>
      <Box maxW="sm" borderWidth="1px" borderRadius="lg" overflow="hidden">
        <Image src={img} alt="Blog Image" />
        <Box p="6" bg="white">
          <Box mt="1" fontWeight="semibold" as="h4" lineHeight="tight" noOfLines={1}>
            {blog?.title}
          </Box>
          <Box mt="1" fontWeight="light" as="p" lineHeight="tight" noOfLines={2}>
            {blog?.content}
          </Box>
          <Box color="gray.500" fontWeight="semibold" letterSpacing="wide" fontSize="xs" textTransform="uppercase" mt={3}>
            {blog?.appreciationCount} Appreciations
          </Box>
        </Box>
      </Box>
    </Link>
  );
}

export default Card;

Blog cards of all blog posts will be displayed on the home page.

Building Pages

Create a new folder pages within the src directory.

Home Page

Create a new a new file pages/Home.js and paste the code below:

import React, { useEffect, useState } from "react";
import { Box, Divider, SimpleGrid } from "@chakra-ui/react";
import Card from "../components/BlogCard";
import { getBlogs } from "../near-blog-api";
import { Layout } from "../components/Layout";

const Home = () => {
  const [blogs, setBlogs] = useState([]);
  const [loading, setLoading] = useState(false);

  const fetchBlogs = async () => {
    try {
      setLoading(true);
      setBlogs(await getBlogs());
      setLoading(false);
    } catch (error) {
      console.log({ error });
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchBlogs();
  }, []);

  return (
    <Layout>
      {" "}
      <Box borderColor="black">
        <Box mt="10" fontSize={24} fontWeight="semibold" as="h1">
          Latests Posts
        </Box>
        <Divider />
        {blogs.length === 0 && (
          <Box mt="10" fontSize={24} fontWeight="semibold" as="h1" textAlign="center">
            No Posts Found
          </Box>
        )}
        {loading ? (
          <Box mt="10" fontSize={24} fontWeight="semibold" as="h1" textAlign="center">
            Loading...
          </Box>
        ) : (
          <SimpleGrid mt="10" columns={2} spacing={10}>
            {blogs?.map((blog, index) => (
              <div key={index}>
                <Card blog={blog} />
              </div>
            ))}
          </SimpleGrid>
        )}
      </Box>
    </Layout>
  );
};

export default Home;

getBlogs returns all the blogs from our contract, this will be displayed on the Home page.

Import the Home page into App.js and add it as a route in the <Routes> component.

import "regenerator-runtime/runtime";
import React from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import Home from "./pages/Home";

export default function App() {
  return (
    <BrowserRouter>
      <Routes>
        <Route path="/" element={<Home />} />
      </Routes>
    </BrowserRouter>
  );
}

Run npm start and navigate to localhost:3000 to view dapp.

Group 21.png

Click the connect button to connect your wallet.

AddBlog Page

The next page to create is the AddBlog page where we can interact with the createBlog function to create a new blog.

Create a new file pages/AddBlog.js and paste the code below:

import React, { useState } from "react";
import { FormControl, FormLabel, Input, Button, Box, Divider, Textarea } from "@chakra-ui/react";
import { createBlog } from "../near-blog-api";
import { Layout } from "../components/Layout";

const AddBlog = () => {
  const [values, setValues] = useState({
    title: "",
    content: "",
    appreciationCost: '1',
  });

  const onChange = (e) => {
    setValues((prev) => ({
      ...prev,
      [e.target.name]: e.target.value,
    }));
  };

  const onSubmit = async () => {
    createBlog(values);
  };

  return (
    <Layout>
      <Box borderColor="black">
        <Box mt="10" fontSize={24} fontWeight="semibold" as="h1">
          Add Blog
        </Box>
        <Divider />
        <FormControl mt="5" isRequired>
          <FormLabel>Title</FormLabel>
          <Input value={values.title} name="title" required onChange={onChange} bg="white" />
        </FormControl>
        <FormControl isRequired>
          <FormLabel>Blog Content</FormLabel>
          <Textarea value={values.content} name="content" onChange={onChange} bg="white" />
        </FormControl>
        <FormControl mt="5" isRequired>
          <FormLabel>Appreciation Cost</FormLabel>
          <Input name="appreciationCost" value={values.appreciationCost} onChange={onChange} bg="white" />
        </FormControl>
        <Button mt={4} colorScheme="teal" type="submit" onClick={onSubmit}>
          Publish
        </Button>
      </Box>
    </Layout>
  );
};

export default AddBlog;

The createBlog function takes the values of title, content, and appreciationCost to create a new blog.

Import the AddBlog page into App.js and add it as a route in the <Routes> component.

<Routes>
    <Route path="/" element={<Home />} />
    <Route path="/add" element={<AddBlog />} />
</Routes>

Click the Add Post button on the navbar and try creating a new blog post. You will be directed to your NEAR wallet to approve the transaction.

Screenshot 2022-08-17 at 3.22.18 AM.png

Visit the Home page and check out your new blog post!

Blog Page

The blog page will display details of a single blog.

Create a new file pages/Blog.js and paste the code below:

import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import { Divider, Box, Button } from "@chakra-ui/react";
import { getBlog, appreciateBlog } from "../near-blog-api";
import { Layout } from "../components/Layout";
import { utils } from 'near-api-js';

const Blog = () => {
  let { id } = useParams();
  const [blog, setBlog] = useState({});
  const [loading, setLoading] = useState(false);

  const fetchBlog = async () => {
    try {
      setLoading(true);
      setBlog(await getBlog({ id: id }));
      setLoading(false);
    } catch (error) {
      console.log({ error });
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    fetchBlog();
  }, []);
  return (
    <Layout>
      {loading ? (
        <Box mt="10" fontSize={24} fontWeight="semibold" as="h1" textAlign="center">
          Loading...
        </Box>
      ) : (
        <Box p="4" boxShadow="xl" rounded="md" bg="white" marginTop="10" py="5">
          <Box borderColor="black">
            <h1 style={{ fontSize: "20px", fontWeight: "bold" }}>{blog?.title}</h1>
            <Box color="gray.500" fontWeight="semibold" letterSpacing="wide" fontSize="xs" textTransform="uppercase" mt={3}>
              Written By {blog?.author}
            </Box>
            <Divider />
          </Box>

          <Box paddingTop="2rem">{blog?.content}</Box>
          <Box color="gray" my="3">
            <p>{blog?.appreciationCount} Appreciations</p>
          </Box>
          <Button onClick={() => appreciateBlog(id, blog?.appreciationCost)}>
            Appreciate with {utils.format.formatNearAmount(blog?.appreciationCost)} NEAR
          </Button>
        </Box>
      )}
    </Layout>
  );
};

export default Blog;

The getBlog function takes in an object of a blog's id and retrieves the stored details from the blockchain.

The 'Appreciate' button calls the appreciateBlog function and directs you to your NEAR wallet to approve a transfer of the appreciationCost to the blog author.

Import the Blog page into App.js and add it as a route in the <Routes> component.

<Routes>
    <Route path="/" element={<Home />} />
    <Route path="/add" element={<AddBlog />} />
    <Route path="/blog/:id" element={<Blog />} />
</Routes>

On the home page, click on a blog card to view the blog on the blog page.

Screenshot 2022-08-25 at 1.36.31 PM.png

Interact with the blockchain by appreciating a blog post!

Visit source code and live link.

Conclusion

Developing smart contracts on the NEAR protocol is very easy to get started with using AssemblyScript. To build NEAR smart contracts with Rust, you have to learn the Rust language.

Looking for resources to learn Rust? I've curated a list.

Visit Near Academy to learn how to build dapps on NEAR.