referencia
源文档La guía completa para el desarrollo Full Stack Web3 - DEV Community
Código fuente, el proyecto github en el artículo fuente no se puede ejecutar directamente, después de la modificación, GitHub se puede usar en mac - daocodedao/web3-blog: https://linzhji.blog.csdn.net/article/details/130125634
marco
El sistema de blog se implementará en el polígono porque el costo de transacción del polígono es relativamente bajo. marco general del proyecto
- Cadena de bloques: Hardhat, polígono
- Entorno de desarrollo ético: Hardhat
- Marcos front-end: Next.js y React
- Almacenamiento de archivos: IPFS
- Búsqueda: el protocolo gráfico
Preparación
- entorno node.js
- vscode
- billetera metamáscara
iniciar el desarrollo
crear proyecto
npx create-next-app web3-blog
cd web3-blog
Enriquecer paquete.json, agregar
"@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 - Entorno de desarrollo de Ethereum
web3modal - Monedero de conexión rápida y conveniente
react-markdown y simplemde - Editor de Markdown y renderizador de markdown para el CMS
@emotion/css - Un gran CSS en la biblioteca JS
@openzeppelin/contracts - Marco de solidez de código abierto
#安装包依赖
npm install
Preparar secuencia de comandos de implementación de cascos
npx hardhat
#选 Create an empty hardhat.config.js
empezar a codificar
Modifique el archivo styles/globals.css, consulte github para obtener el código específico, sin publicar
Agregue logo.svg y right-arrow.svg a la carpeta pública
contrato inteligente
// 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);
_;
}
}
El contrato permite que el propietario cree y edite el contenido del blog, lo que permite que cualquiera pueda obtener el contenido.
contrato de prueba
prueba/muestra-prueba.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
implementar contrato
Inicie la red eth local antes de la implementación
npx hardhat node
Después de que el inicio sea exitoso, puede ver 20 cuentas de prueba, que se pueden usar para el desarrollo de pruebas posteriores
Modifique el script de implementación 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);
});
Ejecutar el despliegue
npx hardhat run scripts/deploy.js --network localhost
Implementación exitosa, dirección del contrato: 0x5fbdb2315678afecb367f032d93f642f64180aa3
monedero meta
Elija una de las direcciones creadas anteriormente
Account #0: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (10000 ETH)
Private Key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
agregar red
Importar cuenta, la clave secreta previamente seleccionada 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
consultar saldo
Aplicación Next.js
Archivo de configuración del entorno
Primero cree el archivo de configuración del entorno.env.local
ENVIRONMENT="local"
NEXT_PUBLIC_ENVIRONMENT="local"
Las variables se pueden cambiar local
, testnet
y mainnet
código js correspondiente
contexto.js
import { createContext } from 'react'
export const AccountContext = createContext(null)
Diseño y navegación
Abra pages/_app.js, modifique, consulte el código de github
Página de entrada del punto de entrada
Abra pages/index.js, consulte el código de github
Publicar página de blog
páginas/create-post.js, consulte el código de github
Ver la página de contenido del blog
Las reglas de dirección detalladas del blog, myapp.com/post/some-post-id, modifican el archivo pages/post/[id].js
editar contenido del blog
Modifique el archivo pages/post/[id].js
Puesta en marcha
npm run dev
O use vscode para depurar
lanzamiento.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"]
}
]
}
Ejecución fallida, error
Error: no se pudo detectar la red (event="noNetwork", code=NETWORK_ERROR, version=providers/5.7.2)
código de búsqueda
provider = new ethers.providers.JsonRpcProvider()
#改为
provider = new ethers.providers.JsonRpcProvider('http://127.0.0.1:8545/')
Este 127.0.0.1:8545 corresponde a la red hardhat anterior
Después de guardar, se informa de nuevo un error
Error: <Enlace> no válido con <a> hijo. Elimine <a> o use <Link legacyBehavior>.
Código de búsqueda <a, correspondiente a find <Link add legacyBehavior
izar
Conecte la billetera metamask
correo
Falló, lea el documento de infura, haga solicitudes - Infura Docs , el uso no es correcto, cámbielo,
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,
},
})
El xxxx del código se aplica en infura
hazlo
Revisa la publicación y reporta un error
La depuración encontró uno más /, elimínelo
El navegador puede acceder a él, pero el código sigue sin funcionar. Revisé el documento de infura y la puerta de enlace pública se ha cerrado. Necesito usar la puerta de enlace para crear un proyecto en infura. El motivo específico: Puerta de enlace pública - Infura Documentos
const ipfsURI = 'https://ipfs.io/ipfs'
#改为
const ipfsURI = 'https://xxx.infura-ipfs.io/ipfs'
# xxx是你自己的gateway
en el polígono
monedero meta
Lista de cadenas
algo de dinero del grifo
desplegar
comentarios abiertos de hardhat.config.js
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]
// }
}
Aquí ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 es la clave privada de la billetera, generada anteriormente
npx hardhat run scripts/deploy.js --network mumbai
URL: "https://polygon-mumbai.blockpi.network/v1/rpc/public",
Disponible en Chainlist
Encontrar un
implementación exitosa
➜ web3-blog git:(main) ✗ npx hardhat run scripts/deploy.js --network mumbai
Blog deployed to: 0x81FeD4CdB0609bE8a23Bc5B95d875c05DD9416E8
tomó algunas pruebas
ejecutar a continuación
Modificar .env.local, cambiar local a testnet
ENVIRONMENT="testnet"
NEXT_PUBLIC_ENVIRONMENT="testnet"
https://rpc-mumbai.matic.today en el código fuente ya no está disponible, cambie a https://polygon-mumbai.blockpi.network/v1/rpc/public
npm run dev
empezar a correr
subgrafo
código fuente fetchPost
y fetchPosts,可以查看某个文章或者全部文章,如果想要搜索文章怎么弄?
El protocolo Graph puede lograr esta función
Crear subgrafos
Inicializar subgráfico a través de la línea de comando Gráfico
comando de ejecución nativo
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 : archivo de configuración para subgraph
- schema.graphql : archivo de sintaxis de GraphQL que define el almacenamiento y el acceso a los datos
- Asignaciones de AssemblyScript : schema.ts Código de AssemblyScript que se traduce de los datos de eventos en Ethereum a las entidades definidas en su esquema (por ejemplo, mapping.ts en este tutorial)
subgraph.yaml
description
(opcional): una descripción legible por humanos de lo que es el subgrafo. El explorador de gráficos muestra esta descripción cuando el subgráfico se implementa en el servicio alojado.repository
(opcional): la URL del repositorio donde se puede encontrar el manifiesto del subgrafo. Esto también se muestra en el Explorador de gráficos.dataSources.source
: la dirección del contrato inteligente, las fuentes del subgráfico y el abi del contrato inteligente que se utilizará. La dirección es opcional; omitirlo permite indexar eventos coincidentes de todos los contratos.dataSources.source.startBlock
(opcional): el número del bloque desde el que la fuente de datos comienza a indexar. En la mayoría de los casos, sugerimos utilizar el bloque en el que se creó el contrato.dataSources.mapping.entities
: las entidades que la fuente de datos escribe en el almacén. El esquema de cada entidad se define en el archivo schema.graphql.dataSources.mapping.abis
: uno o más archivos ABI con nombre para el contrato de origen, así como cualquier otro contrato inteligente con el que interactúe desde las asignaciones.dataSources.mapping.eventHandlers
: enumera los eventos de contratos inteligentes a los que reacciona este subgráfico y los controladores en el mapeo ( ./src/mapping.ts en el ejemplo) que transforman estos eventos en entidades en la tienda.
definir entidades
Defina la entidad en schema.graphql , Graph Node generará una instancia de consulta que incluye la entidad. Cada tipo debe ser una entidad, pasada @entity 声明
las entidades/datos indexarán el Token y el Usuario. Mediante este método podemos indexar los Tokens creados por los usuarios
esquema.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!
}
Construir a través de la línea de comandos
cd blogcms
graph codegen
Actualizar entidades y asignaciones de subgrafos
subgraph.yaml
Asignaciones de scripts de ensamblado
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()
}
}
recompilar
graph build
Desplegando el subgrafo
Encuentra el token del subgrafo
graph auth --product hosted-service 你的suggraph的key
desplegar
yarn deploy
Preguntar
{
posts {
id
title
contentHash
published
postContent
}
}
Sin datos, publiquemos
consulta en la cadena
Dirección del contrato 0x81FeD4CdB0609bE8a23Bc5B95d875c05DD9416E8 | Escaneo de polígonos
Pero suggraph todavía no tiene datos.
Revisa el registro, algo salió mal
¡Después de leer la descripción, debe usarse schema.graphql al definir la entidad publicada! , esto es obligatorio y no puede estar vacío, y no hay ningún parámetro publicado en handlePostCreated, ¡quítelo! ,intentar otra vez
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
}
参考Creación de un subgrafo - The Graph Docs
recompilar, cargar
graph codegen
graph build
yarn deploy
También puede usar la URL después de una implementación exitosa para consultar
para buscar un artículo
{
postSearch(
text: "111"
) {
id
title
contentHash
published
postContent
}
}
gráfico de usos de la aplicación
参考Consultas desde una aplicación - The Graph Docs
No hay un proyecto fuente aquí, agregué una función de búsqueda, no estoy familiarizado con reaccionar, solo míralo
Agregar línea de comando globalmente
npm install --save-dev @graphprotocol/client-cli
paquete.json
añadir
"@apollo/client": "^3.7.12",
npm install
vscode
agregar complemento
buscar.js
Búsqueda directa de código duro 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>
)
}
Realmente no estoy familiarizado con React, ha pasado mucho tiempo
Digresión
En el proceso de usar Next, debido a que no estoy familiarizado con todo el marco, encontré algunos problemas al extraer la clave infura al archivo .env
En el archivo .env.local
INFURA_KEY="xxxxxxxxxxxxx"
Usar en código js
process.env.INFURA_KEY
El resultado es correcto en la consola, pero undefined se imprime en chrome
Solución: https://medium.com/@zak786khan/env-variables-undefined-78cf218dae87
En el archivo .env.local, el nombre de la variable se cambia a
NEXT_PUBLIC_INFURA_KEY="xxxxxxxxxxxxx"
en la línea