Next.js Polygon, Solidity, The Graph, IPFS, Hardhat web3 blog system

reference

源文档The Complete Guide to Full Stack Web3 Development - DEV Community

Source code, the github project in the source article cannot be run directly, after modification, GitHub can be used in mac - daocodedao/web3-blog: https://linzhji.blog.csdn.net/article/details/130125634

frame

The blog system will be deployed on polygon, because the transaction cost of polygon is relatively low. overall project framework

  • Blockchain: Hardhat, polygon
  • eth development environment: Hardhat
  • Front-end frameworks: Next.js and React
  • File storage: IPFS
  • Search: The Graph Protocol

 Preparation

  • node.js environment
  • vscode
  • metamask wallet

start development

create project

 npx create-next-app web3-blog

cd web3-blog

 Enrich package.json, add

    "@openzeppelin/contracts": "^4.3.2",
    "@walletconnect/web3-provider": "^1.6.6",
    "hardhat": "^2.6.7",
    "ipfs-http-client": "^56.0.0",
    "web3modal": "^1.9.4",
    "react-markdown": "^7.1.0",
    "react-simplemde-editor": "^5.0.2",
    "@emotion/css": "^11.5.0"

  },
  "devDependencies": {
    "@nomiclabs/hardhat-ethers": "^2.0.0",
    "@nomiclabs/hardhat-waffle": "^2.0.0",
    "chai": "^4.2.0",
    "eslint": "7",
    "eslint-config-next": "12.0.1",
    "ethereum-waffle": "^3.0.0",
    "ethers": "^5.0.0"
  }

hardhat  - Ethereum development environment
web3modal  - convenient and fast connection wallet
react-markdown  and  simplemde  - Markdown editor and markdown renderer for the CMS
@emotion/css  - A great CSS in JS library
@openzeppelin/contracts  - open source solidity framework

#安装包依赖
npm install

Prepare hardhat deployment script


npx hardhat

#选 Create an empty hardhat.config.js

start coding

Modify  the styles/globals.css file, refer to github for the specific code, no posting

Add logo.svg  and  right-arrow.svg to public folder  

smart contract

// contracts/Blog.sol
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

import "hardhat/console.sol";
import "@openzeppelin/contracts/utils/Counters.sol";

contract Blog {
    string public name;
    address public owner;

    using Counters for Counters.Counter;
    Counters.Counter private _postIds;

    struct Post {
      uint id;
      string title;
      string content;
      bool published;
    }
    /* mappings can be seen as hash tables */
    /* here we create lookups for posts by id and posts by ipfs hash */
    mapping(uint => Post) private idToPost;
    mapping(string => Post) private hashToPost;

    /* events facilitate communication between smart contractsand their user interfaces  */
    /* i.e. we can create listeners for events in the client and also use them in The Graph  */
    event PostCreated(uint id, string title, string hash);
    event PostUpdated(uint id, string title, string hash, bool published);

    /* when the blog is deployed, give it a name */
    /* also set the creator as the owner of the contract */
    constructor(string memory _name) {
        console.log("Deploying Blog with name:", _name);
        name = _name;
        owner = msg.sender;
    }

    /* updates the blog name */
    function updateName(string memory _name) public {
        name = _name;
    }

    /* transfers ownership of the contract to another address */
    function transferOwnership(address newOwner) public onlyOwner {
        owner = newOwner;
    }

    /* fetches an individual post by the content hash */
    function fetchPost(string memory hash) public view returns(Post memory){
      return hashToPost[hash];
    }

    /* creates a new post */
    function createPost(string memory title, string memory hash) public onlyOwner {
        _postIds.increment();
        uint postId = _postIds.current();
        Post storage post = idToPost[postId];
        post.id = postId;
        post.title = title;
        post.published = true;
        post.content = hash;
        hashToPost[hash] = post;
        emit PostCreated(postId, title, hash);
    }

    /* updates an existing post */
    function updatePost(uint postId, string memory title, string memory hash, bool published) public onlyOwner {
        Post storage post =  idToPost[postId];
        post.title = title;
        post.published = published;
        post.content = hash;
        idToPost[postId] = post;
        hashToPost[hash] = post;
        emit PostUpdated(post.id, title, hash, published);
    }

    /* fetches all posts */
    function fetchPosts() public view returns (Post[] memory) {
        uint itemCount = _postIds.current();

        Post[] memory posts = new Post[](itemCount);
        for (uint i = 0; i < itemCount; i++) {
            uint currentId = i + 1;
            Post storage currentItem = idToPost[currentId];
            posts[i] = currentItem;
        }
        return posts;
    }

    /* this modifier means only the contract owner can */
    /* invoke the function */
    modifier onlyOwner() {
      require(msg.sender == owner);
    _;
  }
}

The contract allows the owner to create and edit blog content, allowing anyone to obtain the content

test contract 

test/sample-test.js

onst { expect } = require("chai")
const { ethers } = require("hardhat")

describe("Blog", async function () {
  it("Should create a post", async function () {
    const Blog = await ethers.getContractFactory("Blog")
    const blog = await Blog.deploy("My blog")
    await blog.deployed()
    await blog.createPost("My first post", "12345")

    const posts = await blog.fetchPosts()
    expect(posts[0].title).to.equal("My first post")
  })

  it("Should edit a post", async function () {
    const Blog = await ethers.getContractFactory("Blog")
    const blog = await Blog.deploy("My blog")
    await blog.deployed()
    await blog.createPost("My Second post", "12345")

    await blog.updatePost(1, "My updated post", "23456", true)

    posts = await blog.fetchPosts()
    expect(posts[0].title).to.equal("My updated post")
  })

  it("Should add update the name", async function () {
    const Blog = await ethers.getContractFactory("Blog")
    const blog = await Blog.deploy("My blog")
    await blog.deployed()

    expect(await blog.name()).to.equal("My blog")
    await blog.updateName('My new blog')
    expect(await blog.name()).to.equal("My new blog")
  })
})
npx hardhat test

 deploy contract

Start the local eth network before deployment

npx hardhat node

After the startup is successful, you can see 20 test accounts, which can be used for subsequent test development

Modify the deployment  script scripts/deploy.js

/* scripts/deploy.js */
const hre = require("hardhat");
const fs = require('fs');

async function main() {
  /* these two lines deploy the contract to the network */
  const Blog = await hre.ethers.getContractFactory("Blog");
  const blog = await Blog.deploy("My blog");

  await blog.deployed();
  console.log("Blog deployed to:", blog.address);

  /* this code writes the contract addresses to a local */
  /* file named config.js that we can use in the app */
  fs.writeFileSync('./config.js', `
  export const contractAddress = "${blog.address}"
  export const ownerAddress = "${blog.signer.address}"
  `)
}

main()
  .then(() => process.exit(0))
  .catch((error) => {
    console.error(error);
    process.exit(1);
  });

Execute the deployment

npx hardhat run scripts/deploy.js --network localhost

 Successful deployment, contract address: 0x5fbdb2315678afecb367f032d93f642f64180aa3 

meta wallet

Choose one of the addresses created earlier

Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

add network

Import account, the previously selected secret key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80

check balance

Next.js app 

Environment configuration file

First create the environment configuration file.env.local

ENVIRONMENT="local"
NEXT_PUBLIC_ENVIRONMENT="local"

Variables can be switched localtestnet, and mainnet

js code corresponding

context.js

import { createContext } from 'react'

export const AccountContext = createContext(null)

Layout and Nav

Open pages/_app.js, modify, refer to github code

Entrypoint entry page

Open pages/index.js, refer to the github code

Publish blog page 

pages/create-post.js, refer to github code

View blog content page

Detailed address rules of the blog, myapp.com/post/some-post-id, modify the file pages/post/[id].js

edit blog content

Modify the file pages/post/[id].js

Commissioning

npm run dev

Or use vscode to debug

launch.json

{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Launch via npm",
            "type": "node",
            "request": "launch",
            "cwd": "${workspaceFolder}",
            "runtimeExecutable": "npm",
            "runtimeArgs": ["run-script", "dev"]
          }
          
    ]
}

Execution failed, error

Error: could not detect network (event="noNetwork", code=NETWORK_ERROR, version=providers/5.7.2)

search code

    provider = new ethers.providers.JsonRpcProvider()

#改为

    provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545/')

This 127.0.0.1:8545 corresponds to the previous hardhat network

After saving, an error is reported again

Error: Invalid <Link> with <a> child. Please remove <a> or use <Link legacyBehavior>.

Search code <a, corresponding to find <Link add legacyBehavior

 run up

Connect metamask wallet

post

 Failed, read the infura doc, Make requests - Infura Docs , the use is not right, change it,

const client = create('https://ipfs.infura.io:5001/api/v0')

改为

const projectId = 'xxxxxxx';
const projectSecret = 'xxxxxxxx';
const auth = 'Basic ' + Buffer.from(projectId + ':' + projectSecret).toString('base64');

/* define the ipfs endpoint */
const client = create({
  host: 'ipfs.infura.io',
  port: 5001,
  protocol: 'https',
  headers: {
      authorization: auth,
  },
})

The xxxx of the code is applied in infura

get it done

Check the post and report an error

Debug found one more /, remove it

The browser can access it, but the code is still not working. I checked the doc of infura, and the Public gateway has been closed. I need to use the gateway to create a project on infura. The specific reason: Public gateway - Infura Docs

const ipfsURI = 'https://ipfs.io/ipfs'

#改为

const ipfsURI = 'https://xxx.infura-ipfs.io/ipfs'

# xxx是你自己的gateway

on the polygon

meta wallet

Chainlist

 some money from the tap

Polygon Faucet

deploy

hardhat.config.js open comments

require("@nomiclabs/hardhat-waffle")

/** @type import('hardhat/config').HardhatUserConfig */

module.exports = {
  solidity: "0.8.17",
  networks:{
    hardhat:{
      chainId:1337
    },
    mumbai: {
      url: "https://polygon-mumbai.blockpi.network/v1/rpc/public",
      accounts: ["ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"]
    },
    // polygon: {
    //   url: "https://polygon-rpc.com/",
    //   accounts: [process.env.pk]
    // }
  }

Here ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 is the wallet private key, generated earlier 

npx hardhat run scripts/deploy.js --network mumbai

url: "https://polygon-mumbai.blockpi.network/v1/rpc/public",

Available from Chainlist

Find a

successful deployment

➜  web3-blog git:(main) ✗ npx hardhat run scripts/deploy.js --network mumbai

Blog deployed to: 0x81FeD4CdB0609bE8a23Bc5B95d875c05DD9416E8

took some testing 

run next

Modify  .env.local, change local to testnet

ENVIRONMENT="testnet"
NEXT_PUBLIC_ENVIRONMENT="testnet"

https://rpc-mumbai.matic.today in the source code is no longer available, change to https://polygon-mumbai.blockpi.network/v1/rpc/public

npm run dev

start running

subgraph

source code fetchPost and fetchPosts,可以查看某个文章或者全部文章,如果想要搜索文章怎么弄?

The Graph  protocol can achieve this function

Create subgraphs

Initialize subgraph via the Graph command line

native execution command

npm install -g @graphprotocol/graph-cli
#命令参考
graph init --from-contract your-contract-address \
--network mumbai --contract-name Blog --index-events


#具体命令
graph init --from-contract 0x81FeD4CdB0609bE8a23Bc5B95d875c05DD9416E8 \
--network mumbai --contract-name Blog --index-events

  • subgraph.yaml : configuration file for subgraph
  • schema.graphql : GraphQL syntax file that defines data storage and access
  • AssemblyScript Mappings: schema.ts AssemblyScript code that translates from the event data in Ethereum to the entities defined in your schema (e.g. mapping.ts in this tutorial)

subgraph.yaml

  • description (optional): a human-readable description of what the subgraph is. This description is displayed by the Graph Explorer when the subgraph is deployed to the Hosted Service.
  • repository (optional): the URL of the repository where the subgraph manifest can be found. This is also displayed by the Graph Explorer.
  • dataSources.source: the address of the smart contract the subgraph sources, and the abi of the smart contract to use. The address is optional; omitting it allows to index matching events from all contracts.
  • dataSources.source.startBlock (optional): the number of the block that the data source starts indexing from. In most cases we suggest using the block in which the contract was created.
  • dataSources.mapping.entities : the entities that the data source writes to the store. The schema for each entity is defined in the the schema.graphql file.
  • dataSources.mapping.abis: one or more named ABI files for the source contract as well as any other smart contracts that you interact with from within the mappings.
  • dataSources.mapping.eventHandlers: lists the smart contract events this subgraph reacts to and the handlers in the mapping — ./src/mapping.ts in the example — that transform these events into entities in the store.

define entities

Define entity in  schema.graphql   , Graph Node will generate query instance including entity. Each type must be an entity, passed @entity 声明

entities / data will index Token and User. Through this method we can index Tokens created by users

schema.graphql 

type _Schema_
  @fulltext(
    name: "postSearch"
    language: en
    algorithm: rank
    include: [{ entity: "Post", fields: [{ name: "title" }, { name: "postContent" }] }]
  )

type Post @entity {
  id: ID!
  title: String!
  contentHash: String!
  published: Boolean!
  postContent: String!
  createdAtTimestamp: BigInt!
  updatedAtTimestamp: BigInt!
}

Build via command line

cd blogcms
graph codegen

Update subgraph entities and mappings

subgraph.yaml 

 Assemblyscript mappings

import {
  PostCreated as PostCreatedEvent,
  PostUpdated as PostUpdatedEvent
} from "../generated/Blog/Blog"
import {
  Post
} from "../generated/schema"
import { ipfs, json } from '@graphprotocol/graph-ts'

export function handlePostCreated(event: PostCreatedEvent): void {
  let post = new Post(event.params.id.toString());
  post.title = event.params.title;
  post.contentHash = event.params.hash;
  let data = ipfs.cat(event.params.hash);
  if (data) {
    let value = json.fromBytes(data).toObject()
    if (value) {
      const content = value.get('content')
      if (content) {
        post.postContent = content.toString()
      }
    }
  }
  post.createdAtTimestamp = event.block.timestamp;
  post.save()
}

export function handlePostUpdated(event: PostUpdatedEvent): void {
  let post = Post.load(event.params.id.toString());
  if (post) {
    post.title = event.params.title;
    post.contentHash = event.params.hash;
    post.published = event.params.published;
    let data = ipfs.cat(event.params.hash);
    if (data) {
      let value = json.fromBytes(data).toObject()
      if (value) {
        const content = value.get('content')
        if (content) {
          post.postContent = content.toString()
        }
      }
    }
    post.updatedAtTimestamp = event.block.timestamp;
    post.save()
  }
}

recompile

graph build

Deploying the subgraph

Find the token of the subgraph

 

graph auth --product hosted-service 你的suggraph的key

deploy

yarn deploy

Inquire

{
  posts {
    id
    title
    contentHash
    published
    postContent
  }
}

No data, let's post

 

Query on the chain

Contract Address 0x81FeD4CdB0609bE8a23Bc5B95d875c05DD9416E8 | PolygonScan

But suggraph still has no data

 Check the log, something went wrong

 After reading the description, schema.graphql should be used when defining the published entity! , this is mandatory and cannot be empty, and there is no published parameter in handlePostCreated, remove it! ,try again

type _Schema_
  @fulltext(
    name: "postSearch"
    language: en
    algorithm: rank
    include: [{ entity: "Post", fields: [{ name: "title" }, { name: "postContent" }] }]
  )

type Post @entity {
  id: ID!
  title: String
  contentHash: String
  published: Boolean
  postContent: String
  createdAtTimestamp: BigInt
  updatedAtTimestamp: BigInt
}

参考Creating a Subgraph - The Graph Docs

recompile, upload

graph codegen
graph build
yarn deploy

You can also use the url after successful deployment to query

 

to search for an article

{
  postSearch(
    text: "111"
  ) {
    id
    title
    contentHash
    published
    postContent
  }
}

 

app uses graph

参考Querying from an Application - The Graph Docs 

There is no source project here, I added a search function, I am not familiar with react, just look at it 

Add command line globally

npm install --save-dev @graphprotocol/client-cli

package.json

add in

    "@apollo/client": "^3.7.12",
npm install

vscode

add plugin

search.js

Direct hardcode search 111

import { useState, useEffect } from 'react'
import { ApolloClient, InMemoryCache, gql } from '@apollo/client'

// https://api.thegraph.com/subgraphs/name/daocodedao/blogcms
const APIURL = 'https://api.thegraph.com/subgraphs/name/daocodedao/blogcms'

const blogQuery = `
{
    postSearch(
      text: "111"
    ) {
      id
      title
      contentHash
      published
      postContent
    }
}
`

const client = new ApolloClient({
  uri: APIURL,
  cache: new InMemoryCache(),
})

export default function Search() {
    const [searchCount, setSearchCount] = useState(0)
    client.query({
        query: gql(blogQuery),
    })
    .then((data) => {
        console.log('Subgraph data: ', data)
        setSearchCount(data?.data?.postSearch?.length)
    })
    .catch((err) => {
        console.log('Error fetching data: ', err)
    })
    return (
        <div>搜索条件是:111, 共有
        {
            searchCount
        }
        条数据
        </div>
    )
}

 I'm really not familiar with React, it's been a long time

Digression

In the process of using Next, because I am not familiar with the whole framework, I encountered some problems when extracting the infura key to the .env file

In the .env.local file

INFURA_KEY="xxxxxxxxxxxxx"

Use in js code

process.env.INFURA_KEY

The result is correct in the console, but undefined is printed in chrome

Solution: https://medium.com/@zak786khan/env-variables-undefined-78cf218dae87

In the .env.local file, the variable name is changed to


NEXT_PUBLIC_INFURA_KEY="xxxxxxxxxxxxx"

on the line 

Guess you like

Origin blog.csdn.net/linzhiji/article/details/130125634