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 local
, testnet
, 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
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