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

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 localtestnetmainnet

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

Grifo Polígono

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 

Supongo que te gusta

Origin blog.csdn.net/linzhiji/article/details/130125634
Recomendado
Clasificación