Introdução
Este tutorial assume um conhecimento básico dos conceitos de React, Typescript e Blockchain, como carteira e chave privada.
O principal objetivo deste projeto é criar um verdadeiro dApp que utilize Cedalio como uma plataforma de armazenamento. Escolhemos a Polygon-Mumbai como nossa rede. Se você quiser, pode escolher outra cadeia que nós suportamos.
Se você quiser pular no código, por favor, seja nosso convidado. Ou você pode usar nossa demonstração online.
Requisitos
- Carteira Metamask (No momento suportamos Ethereum, Polygon, Metis e Hard Hat) Para este exemplo, usaremos a Polygon Mumbai. Por favor, acrescente a rede Polygon-Mumbai à sua carteira.
- NodeJS 14v
- NPM ou YARN
- NPX
- Ferramentas para desenvolvedores React
- Material UI (style component)
- Web3modal (Wallet Connect)
- Beautiful dnd react
- Apollo Client Devtool
- Cedalio CLI
Começando
Neste tutorial, queremos mostrar como criar um aplicativo de tarefas (To Do app) que usa o Cedalio como camada de armazenamento. Nós nos concentramos em armazenar dados on-chain e consumi-los com um cliente usando um esquema simples de GraphQL e um cliente como ApolloClient em uma aplicação React. Obtemos os dados da blockchain usando o cliente Apollo GraphQL implementado em TypeScript. Decidimos criar um Contrato Inteligente de Banco de Dados para todos os usuários e filtrar as tarefas de cada cliente no frontend.
Para acelerar o tutorial, recomendamos clonar o repositório e segui-lo junto com o código-fonte.
Usamos o WalletConnect para fazer o login em nosso aplicativo de tarefase filtrar as tarefas criadas por sua carteira. Para obter um projectId você deve se inscrever aqui e criar um projeto. Uma vez que você tenha o projectId, crie um arquivo .env para o dApp de tarefas Cedalio com uma variável de ambiente chamada REACT_APP_WC_PROJECT_ID. Saiba mais sobre o WalletConnect.
Em cada componente criado neste tutorial, temos 3 seções principais: as definições de consulta GraphQL com a notação gql, estados para lidar com as mudanças e entradas do usuário, e os componentes JSX usados para renderizar.
Note que utilizamos o gancho (hook) useAccount. Isto nos permite saber se a carteira já está conectada ou não, e enviá-la para ListComponent, que valida se o endereço existe (usuário está conectado) para renderizar a lista de tarefas.
const { address } = useAccount()
Definir a arquitetura do dApp
Temos duas abordagens diferentes de arquitetura utilizando o Cedalio. Você pode ter um banco de dados de contratos inteligentes para cada usuário, ou um banco de dados de contratos inteligentes para todos os usuários. Na primeira opção todos os usuários são os proprietários do contrato inteligente que detém as informações, e na outra opção o administrador do dApp é o proprietário do banco de dados do contrato inteligente. Para este exemplo, escolhemos a segunda opção (um banco de dados para todos os usuários) para simplificar o exemplo e focar na criação de um dApp de ponta a ponta usando nossa tecnologia. No próximo post, falaremos sobre como implementar um banco de dados para cada usuário.
Temos React + Apollo como nosso cliente de frontend e Cedalio como nosso backend GraphQL, usando a Polygon Mumbai como rede.
Definir o schema do GraphQL
Primeiro, precisamos definir nosso esquema básico do GraphQL, criamos um tipo de objeto para armazenar as tarefas e um arquivo chamado todo.graphql
O esquema é parecido com este:
type Todo {
id: UUID!
title: String!
description: String
priority: Int!
owner: String!
tags: [String!]
status: String
}
Então podemos usar a CLI do Cedalio para compilar o esquema para verificar se está correto. Se você ainda não tem nossa CLI instalada, siga estes passos.
>bifrost compile --schema-file /path/to/your/todo.graphql
Algo útil é exportar o esquema para ver todas as mutações e consultas que o Cedalio cria automaticamente para seu esquema. Você também pode revisar os tipos de entrada para todas as mutações. Para exportar o esquema executado:
bifrost export-schema --schema-file /path/to/your/todo.graphq
Este é o resultado que você obtém. Se preferir, você pode redirecionar a saída para um arquivo :D:
scalar UUID
enum RelationType {
OWNS
REFERENCES
EMBEDDED
BELONGS_TO
}
directive @belongsTo(type: String!) on OBJECT
directive @relation(type: String!, via: String) on FIELD_DEFINITION
directive @embedded on FIELD_DEFINITION
directive @belongsTo(via: String!) on FIELD_DEFINITION
type Todo {
id: UUID!
title: String!
description: String
priority: Int!
owner: String!
tags: [String!]
status: String
}
input TodoInput {
title: String!
description: String
priority: Int!
owner: String!
tags: [String!]
status: String
}
input TodoUpdateInput {
title: String
description: String
priority: Int
owner: String
tags: [String!]
status: String
}
type Query {
todoById(id: UUID!): Todo
allTodos: [Todo!]!
}
type Mutation {
createTodo(todo: TodoInput!): Todo!
updateTodo(id: UUID!, fields: TodoUpdateInput!): Todo!
}
schema {
query: Query
mutation: Mutation
}
Implementando seu Esquema
Uma vez definido seu esquema, você pode implementá-lo e começar a executar mutações e consultas contra ele. Atualmente, para implantar seu esquema, você precisa de uma carteira com tokens suficientes da blockchain onde você deseja implantá-lo. Há muitas opções para escolher. Recomendamos a MetaMask.
Agora você está pronto para implantar e servir seu esquema, mas primeiro temos que definir uma variável de ambiente - nosso endereço de chave privada da Metamask - . Seja muito cauteloso ao utilizar esta chave. Tenha em mente que só acessamos sua chave privada para assinar as transações que são enviadas para a blockchain de sua escolha ao implantar um esquema ou executar uma mutação. Aqui você pode encontrar mais informações sobre isto.
**
export PRIVATE_KEY='your-private-key'
**```
**
Por favor, verifique se você tem tokens suficientes para implantar este contrato inteligente e para executar transações futuras. Se você precisar de mais tokens, você tem duas opções: A [Torneira Polygon](https://faucet.polygon.technology) ou a [Torneira Alchemy](https://mumbaifaucet.com).
![Image](https://web3dev-forem-production.s3.amazonaws.com/uploads/articles/egnwt8z17ud396ur7oyb.png)
bifrost deploy --schema-file /path/to/your/todo.graphql --network polygon-mumbai
Please enter the schema name: todo-v1
✅ Schema '/Users/nicomagni/Desktop/Cedalio/ToDo/todo.graphql' was successfully compiled!
Deploying Graphql schema 'todo.graphql' with wallet address '0x#########################################' in network 'polygon-mumbai'... 🚀
📣 deploying may take between 10 to 30 seconds depending on network congestion
✅ Schema 'todo-v1' was successfully deployed!
The configuration file was saved at: '/Users/USER/.bifrost/todo-v1/deployment.json'
The smart-contract database address is: '0xd8771c6e0c3f01fd78b5e9c301893c4633180fc9'
The address of the library contract is: '0xb3eb32b495ecf76c3788e7a6acafdca0952d4654'
You can see the database contract here: 'https://mumbai.polygonscan.com/address/0xd8771c6e0c3f01fd78b5e9c301893c4633180fc9'
You can launch GraphQL server for this schema by executing
bifrost serve --schema-name todo-v1
Uma vez implantado seu esquema, você está pronto para [iniciar um servidor GraphQL local](https://docs.cedalio.com/quickstart/getting-started/run-locally#serve-your-graphql-schema) e executar consultas e mutações. Este servidor local do GraphQL destina-se a ser usado para fins de desenvolvimento e testes. Para iniciar o servidor, tudo o que você precisa fazer é executar o seguinte:
![Image](https://web3dev-forem-production.s3.amazonaws.com/uploads/articles/nsgnxxb3myvnl0uu1brw.png)
bifrost serve --schema-name todo-v1
Como mostrado no log de saída, você pode copiar o endereço do banco de dados do contrato inteligente no explorador da blockchain. Isto é útil para entender como as transações são executadas contra o banco de dados do contrato inteligente.
Uma vez que um servidor GraphQL esteja rodando, você pode executar consultas e mutações. Por padrão, o endpoint do GraphQL está disponível em `http://localhost:8080/graphql`. Com o servidor em execução, agora podemos criar um objeto `ToDo` no esquema de tarefas executando a mutação `createTodo` que é gerada automaticamente pelo Bifrost.
Se você quiser testar seu servidor, você pode criar o item ToDo em execução:
curl -v -H 'Content-Type: application/json' \
-d '{"query":"mutation { createTodo(todo: { title:\"Something\", priority: 1, owner:\"0x305d4bcaE378094F1923f8BB352824D9496510b1\",tags:[\"health\",\"rutine\"]\n}) { id title tags} }"}' \
'[http://localhost:8080/graphql](http://localhost:8080/graphql)'
E obtenha todos as tarefas executando:
curl -v -H 'Content-Type: application/json' \
-d '{"query":"query { allTodos { id title tags status } }"}' \
'[http://localhost:8080/graphql](http://localhost:8080/graphql)'
## Criando uma aplicação React
Agora vamos desenvolver o aplicativo React. Se preferir, você pode iniciar um aplicativo [do zero](https://github.com/facebook/create-react-app) ou [clonar nosso repositório](https://github.com/cedalio/To-Do-single-db) e seguir as instruções abaixo para entender como ele funciona.
## Adicionar uma tarefa
Em nosso TodoInputComponent temos que usar o apollo-cliente para fazer a mutação que cria uma nova tarefa. Primeiro definimos nossa mutação em nosso **TodoInputComponent.tsx:**
**
```**
const CREATE_TODO = gql`
mutation CreateTodo($title:String!, $description:String, $priority:Int!, $owner:String!,$tags:String, $status:String){
createTodo(todo: {title: $title, description:$description, priority:$priority, owner:$owner, tags:$tags, status: $status }){
id
title
description
priority
owner
tags
status
}
}
`;
Depois criamos as variáveis e tipos que precisamos (título, descrição, prioridade, proprietário, etiqueta e status) e depois as adicionamos à mutação createTodo. Você pode usar o esquema exportado para verificar os tipos de entrada de mutação necessários.
Estamos usando os estados do React para lidar com as entradas e erros do usuário:
const [priority, setPriority] = React.useState("");
const [title, setTitle] = React.useState("");
const [description, setDescription] = React.useState("");
const [titleError, setTitleError] = React.useState(false);
const [descriptionError, setDescriptionError] = React.useState(false);
const [tag, setTag] = React.useState<string[]>([]);
const priorities = [1, 2, 3, 4]
Estamos usando o gancho useMutation do cliente Apollo para vincular a execução de nossa mutação:
const [createTodo, { data, loading, error }] = useMutation(CREATE_TODO);
Renderize as tarefas
Primeiro precisamos definir nosso tipo 'todo' para lidar com as tarefas em toda a aplicação.
type Todo = {
title: string,
description?: string,
tags: Array<string>,
priority: number,
id: string,
owner: string,
status: string
}
Então, precisamos definir nossa consulta na ListComponent.tsx:
**
const GET_TODOS = gql`
query GetTodos{
allTodos {
id
title
description
priority
owner
tags
status
}
}
`;
Para apresentar a lista de tarefas criadas em nossa conta, usamos o gancho useQuery do apollo-cliente.
const { loading, error, data } = useQuery(GET_TODOS);
React.useEffect(() => {
if (data) setTodos(data.allTodos);
}, [data]);
if (loading) return <p>Loading...</p>;
if (error) {
return <p>Error : {error.message}</p>;
Crie um componente que exibe as tarefas como cards:
todos.filter((todo) => todo.status === "ready").map((todo: Todo, index) => (
<CardComponent key={todo.id}
todo={todo}
ownerAddress={ownerAddress}
setState={setTodos}
index={index}
updateState={update}
onUpdateTodo={onUpdateTodo} />
))
Definimos o status como "ready"(pronto) como o padrão. Os outros estados válidos são "done" (Feito) e "delete" (apagar).
Marcar como Done e Delete
Usamos a mutação de atualização para mudar o status de uma tarefa, estamos usando o mesmo padrão de design que usamos no passado. O CardComponent.tsx se parece com isto:
const UPDATE_TODO = gql`
mutation UpdateTodo($id:UUID!, $status:String){
updateTodo(id: $id, fields:{status: $status} ){
id
title
description
priority
owner
tags
status
}
}
`;
Isto recebe 2 variáveis, a UUID da tarefa que queremos mudar e o campo (neste caso, o status) que precisamos atualizar. Como já fizemos anteriormente, precisamos da função updateTodo do gancho useMutation
const [updateTodo, { data, loading, error }] = useMutation(UPDATE_TODO);
Também queremos executar a mutação para atualização quando o usuário arrasta a tarefa nas zonas delete/done, isto requer a bela biblioteca dnd, que nos permite definir nosso cartão de tarefas como arrastável (draggable), e as 3 colunas que precisamos (delete, ready e done) como soltáveis (droppable). O cartão da tarefa deve ter a etiqueta <Draggable />
<DragDropContext onDragEnd={onDragEnd} onDragStart={onDragStart} onDragUpdate={onDragUpdate}>
<div className="container">
<Droppable droppableId='delete'>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} className="delete-container">
<HighlightOffIcon fontSize="large" sx={{ height: "200px", width: "200px", color: deleteIconColor }} />
{provided.placeholder}
</div>
)}
</Droppable>
<Droppable droppableId='ready'>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} className="list-container">
{displayTodos()}
{provided.placeholder}
</div>
)}
</Droppable>
<Droppable droppableId='done'>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef} className="done-container">
<CheckCircleOutlineIcon fontSize="large" sx={{ height: "200px", width: "200px", color: doneIconColor }} />
{provided.placeholder}
</div>
)}
</Droppable>
</div>
<TodoInputComponent setState={setNewTodo} address={ownerAddress} />
<Snackbar open={open} autoHideDuration={6000} onClose={handleClose}>
<Alert onClose={handleClose} severity="success" sx={{ width: '100%' }}>
A operação foi um sucesso!
</Alert>
</Snackbar>
</DragDropContext>
Adicionar um Estado Vazio
Precisamos ter um item de tarefa com estado vazio para nossos usuários quando eles chegam e não temos nenhum item de tarefa ainda criado.
const defaultTodo = {
title: "Este é o seu primeiro cartão de tarefas
!",
description: "Comprar comida para o meu cachorro e trocar a água
",
tags: ["healt", "rutine"],
priority: 1,
id: "abcdefg12345",
owner: "abcdefg123",
status: "ready"
}
E incluí-lo na função displayTodos. Observe que filtramos os resultados da consulta e consideramos "exibíveis" (displayable) os que têm status = ready.
const displayTodos = () => {
const displayableTodos = todos.filter((todo) => todo.status === "ready")
if (displayableTodos.length === 0) {
return <CardComponent key="default" todo={defaultTodo} ownerAddress={defaultTodo.owner} setState={setTodos} index={1} updateState={update} onUpdateTodo={onUpdateTodo} default={true}/>;
}
else {
return (
displayableTodos.map((todo: Todo, index) => (
<CardComponent key={todo.id} todo={todo} ownerAddress={ownerAddress} setState={setTodos} index={index} updateState={update} onUpdateTodo={onUpdateTodo} default={false}/>
))
)
}
}
Quando a transação na blockchain é feita e confirmada, vemos uma notificação (toast) na parte inferior esquerda como uma mensagem para o usuário.
Conclusão
Com o Cedalio podemos criar um dApp apenas definindo o modelo de dados e o Cedalio cria automaticamente os resolvedores para você. Você também pode armazenar todas as informações on-chain sem se debater com o Solidity ou contratos inteligentes.
Finalmente, você também pode escolher uma rede ou cadeia diferente para implantar seu DB (Bando de Dados) apenas mudando o sinalizador de implantação e isso é tudo o que você precisa fazer.
Artigo escrito no Cedalio . A versão original pode ser encontrada aqui. Traduzido e adaptado por Dimitris Calixto.
Latest comments (0)