WEB3DEV

Cover image for Polygon: faça um aplicativo de doação com IPFS e Polygon
Diogo Jorge
Diogo Jorge

Posted on

Polygon: faça um aplicativo de doação com IPFS e Polygon

Este guia foi originalmente publicado em https://docs.filebase.com. Confira o site de documentação do Filebase para obter os tutoriais Web3 mais recentes usando o IPFS por meio do Filebase.

Aprenda a criar um aplicativo de doação Web3 usando IPFS e Polygon.

O que é a Polygon?

A Polygon é uma rede blockchain de camada dois que é projetada como uma solução sidechain para escalar junto com a rede blockchain Ethereum.

Leia abaixo para saber como criar um aplicativo de doação em Web3 usando IPFS e Polygon.

Pré-requisitos:

  • Uma carteira criptográfica, como MetaMask.
  • Configure sua carteira criptográfica para usar a Rede Polygon Mumbai.
  • Instalar Node.js e npm.
  • Baixe e instale um IDE de sua escolha, como VSCode.
  • Inscrever-se para uma conta gratuita do Filebase.
  • Tenha seu acesso à base de arquivos e chaves secretas. Saiba como visualizar suas chaves de acesso aqui.
  • Crie um bucket Filebase. Aprenda a criar um bucket aqui.

1. Comece instalando o pacote Truffle NPM com o seguinte comando:

npm install -g truffle
Enter fullscreen mode Exit fullscreen mode

2. Em seguida, crie um aplicativo NextJS com os comandos:

npm install next
npx create-next-app --typescript
Enter fullscreen mode Exit fullscreen mode

Quando solicitado, forneça um nome para seu aplicativo. Este tutorial usa o nome ‘polygon-dapp’.

3. Navegue até o diretório do aplicativo recém-criado e inicialize o projeto:

cd polygon-dapp
truffle init
Enter fullscreen mode Exit fullscreen mode

O comando ‘init’ criará os seguintes arquivos e diretórios:

  • contracts/: Usado para armazenar os contratos inteligentes do projeto.
  • migrations/: Usado para armazenar os scripts Truffle para implantar os contratos inteligentes.
  • test/: Usado para armazenar os scripts de teste do projeto para validar os contratos inteligentes.
  • truffle-config.js: O arquivo de configuração de Truffle.

4. Navegue até o diretório contracts e crie um novo arquivo chamado DonationContract.sol.

No arquivo, insira o seguinte código:

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.5.16;

contract DonationContract {
 // Acompanha o número total de imagens no contrato
 uint256 public imageCount =  0;

 // Estrutura de dados para armazenar os dados das imagens

 struct Image {
   uint256 id;
   string hash;
   string description;
   uint256 donationAmount;
   address payable author;
 }

 mapping(uint256 => Image) public images;

 // Evento emitido quando a imagem é criada
 event ImageCreated(
   uint256 id,
   string hash,
   string description,
   uint256 donationAmount,
   address payable author
 );

 // Evento emitido quando há uma doação
 event DonateImage(
   uint256 id,
   string hash,
   string description,
   uint256 donationAmount,
   address payable author
 );

// Crie uma imagem
function uploadImage(string memory _imgHash, string memory _description) public {
 require(bytes(_imgHash).length > 0);
 require(bytes(_description).length > 0);
 require(msg.sender != address(0x0));
 imageCount++;
 images[imageCount] = Image(
   imageCount,
   _imgHash,
   _description,
   0,
   msg.sender
 );
 emit ImageCreated(imageCount, _imgHash, _description, 0, msg.sender);
}

function donateImageOwner(uint256 _id) public payable {
 require(_id > 0 && _id <= imageCount);

 Image memory _image = images[_id];
 address payable _author = _image.author;
 address(_author).transfer(msg.value);
 _image.donationAmount = _image.donationAmount + msg.value;
 images[_id] = _image;

 emit DonateImage(
   _id,
   _image.hash,
   _image.description,
   _image.donationAmount,
   _author
 );
}
}
Enter fullscreen mode Exit fullscreen mode

Este contrato inteligente lida com o processo de doação quando os usuários interagem com nosso aplicativo. Salve este arquivo e compile-o com o comando:

truffle compile
Enter fullscreen mode Exit fullscreen mode

Você deve receber uma saída semelhante à seguinte:

Image description

5. Navegue até o diretório migrations e, em seguida, crie um novo arquivo chamado 1_initial_migration.js.

Neste arquivo, insira o seguinte código:

const Migrations = artifacts.require("Migrations");

module.exports = function(deployer) {
 deployer.deploy(Migrations);
};
Enter fullscreen mode Exit fullscreen mode

6. Salve este arquivo. Em seguida, navegue de volta para o diretório contracts e crie um novo arquivo chamado Migrations.sol.

Insira o seguinte código neste arquivo:

pragma solidity >=0.4.22 <0.7.0;

contract Migrations {
 address public owner;
 uint256 public last_completed_migration;

 modifier restricted() {
   if (msg.sender == owner) _;
 }

 constructor() public {
   owner = msg.sender;
 }

 function setCompleted(uint completed) public restricted {
   last_completed_migration = completed;
 }

 function upgrade(address new_address) public restricted {
   Migrations upgraded = Migrations(new_address);
   upgraded.setCompleted(last_completed_migration);
 }
}
Enter fullscreen mode Exit fullscreen mode

7. No diretório migrations, crie outro novo arquivo chamado 2_donation_contract_migration.js.

Neste arquivo, insira o seguinte código:

const DonationContract = artifacts.require("DonationContract");
module.exports = function (deployer) {
deployer.deploy(DonationContract);
};
Enter fullscreen mode Exit fullscreen mode

8. Em seguida, instale o Ganache.

Você pode tanto instalar o aplicativo GUI ou a ferramenta CLI. Vamos usar o aplicativo GUI. Após o download, inicie o aplicativo GUI e selecione ‘Quickstart Ethereum’ quando solicitado a criar um espaço de trabalho.

Image description

9. Em seguida, selecione 'Salvar' para iniciar seu ambiente blockchain de desenvolvimento Ganache.

Image description

10. Em seguida, abra o arquivo truffle-config.js e substitua seu conteúdo existente pelo seguinte:

module.exports = {
 networks: {
   development: {
     host: "localhost",
     port: 7545,
     network_id: "*",
   },
 },
 contracts_directory: "./contracts",
 contracts_build_directory: "./abis",
 compilers: {
   solc: {
     optimizer: {
       enabled: true,
       runs: 200,
     },
   },
 },

 db: {
   enabled: false,
 },
};
Enter fullscreen mode Exit fullscreen mode

11. Agora é hora de implantar nossa configuração Truffle na rede de desenvolvimento Ganache com o comando:

truffle migrate
Enter fullscreen mode Exit fullscreen mode

A saída deve se parecer com o seguinte:

Image description

12. Antes de prosseguir com nosso aplicativo, executaremos alguns scripts de teste. No diretório tests, crie um novo arquivo chamado donation-contract-tests.js que contém o seguinte código:

const { assert } = require("chai");
const DonationContract = artifacts.require("./DonationContract.sol");
require("chai").use(require("chai-as-promised")).should();

contract("DonationContract", ([deployer, author, donator]) => {
 let donationContract;
 before(async () => {
   donationContract = await DonationContract.deployed();
 });

 describe("deployment", () => {
   it("should be an instance of DonationContract", async () => {
     const address = await donationContract.address;
     assert.notEqual(address, null);
     assert.notEqual(address, 0x0);
     assert.notEqual(address, "");
     assert.notEqual(address, undefined);
   });
 });

 describe("Images", () => {
   let result;
   const hash = "abcd1234";
   const description = "This is a test image";
   let imageCount;
   before(async () => {
     result = await donationContract.uploadImage(hash, description, {
       from: author,
     });
     imageCount = await donationContract.imageCount();
   });

   it("Check Image", async () => {
     let image = await donationContract.images(1);
     assert.equal(imageCount, 1);
     const event = result.logs[0].args;
     assert.equal(event.hash, hash);
     assert.equal(event.description, description);
   });

   it("Allow users to donate", async () => {
     let oldAuthorBalance;
     oldAuthorBalance = await web3.eth.getBalance(author);
     oldAuthorBalance = new web3.utils.BN(oldAuthorBalance);
     result = await donationContract.donateImageOwner(imageCount, {
       from: donator,
       value: web3.utils.toWei("1", "Ether"),
     });

     const event = result.logs[0].args;
     let newAuthorBalance;
     newAuthorBalance = await web3.eth.getBalance(author);
     newAuthorBalance = new web3.utils.BN(newAuthorBalance);

     let donateImageOwner;
     donateImageOwner = web3.utils.toWei("1", "Ether");
     donateImageOwner = new web3.utils.BN(donateImageOwner);

     const expectedBalance = oldAuthorBalance.add(donateImageOwner);
     assert.equal(newAuthorBalance.toString(), expectedBalance.toString());
   });
 });
});
Enter fullscreen mode Exit fullscreen mode

13. Em seguida, instale o pacote chai, que é o pacote de script de teste do Truffle usado no script acima:

npm install chai chai-as-promised
Enter fullscreen mode Exit fullscreen mode

14. Em seguida, execute o script de teste com o comando:

truffle test
Enter fullscreen mode Exit fullscreen mode

Sua saída deve ficar assim:

Image description

15. No diretório raiz do seu projeto, crie um novo diretório chamado contexts, em seguida, crie um novo arquivo chamado DataContext.tsx.

Neste arquivo, insira o seguinte conteúdo:

declare let window: any;
import { createContext, useContext, useEffect, useState } from "react";
import Web3 from "web3";
import DonationContract from "../abis/DonationContract.json";

interface DataContextProps {
 account: string;
 contract: any;
 loading: boolean;
 images: any[];
 imageCount: number;
 updateImages: () => Promise<void>;
 donateImageOwner: (id: string, donateAmout: any) => Promise<void>;
}

const DataContext = createContext<DataContextProps | null>(null);

export const DataProvider: React.FC = ({ children }) => {
 const data = useProviderData();
 return <DataContext.Provider value={data}>{children}</DataContext.Provider>;
};

export const useData = () => useContext<DataContextProps | null>(DataContext);
Enter fullscreen mode Exit fullscreen mode

Este script usa o pacote Web3, então vamos instalá-lo com o comando:

npm install web3
Enter fullscreen mode Exit fullscreen mode

16. Em seguida, abra o arquivo pages/_app.tsx e insira o seguinte código:

import "tailwindcss/tailwind.css";
import { DataProvider } from "../contexts/DataContext";

function MyApp({ Component, pageProps }) {
 return (
   <>
     <DataProvider>
       <Component {...pageProps} />
     </DataProvider>
   </>
 );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

17. Para a interface de usuário do nosso aplicativo, usaremos o pacote TailwindCSS. Instale-o, junto com HeadlessUI, com o comando:

npm install tailwindcss @headlessui/react
Enter fullscreen mode Exit fullscreen mode

18. Abra o arquivo pages/index.tsx e substitua o código existente pelo seguinte conteúdo:

import Head from "next/head";
import { useState } from "react";
import Body from "../components/Body";
import Header from "../components/Layout/Header";
import { UploadImage } from "../components/UploadImage";
import { useData } from "../contexts/DataContext";

export default function Home() {
 let [isOpen, setIsOpen] = useState(false);
 const { loading } = useData();

 function closeModal() {
   setIsOpen(false);
 }

 function openModal() {
   setIsOpen(true);
 }

 return (
   <div className="flex flex-col items-center justify-start min-h-screen py-2">
     <Head>
       <title>Decentragram</title>
       <link rel="icon" href="/favicon.ico" />
     </Head>
     <UploadImage isOpen={isOpen} closeModal={closeModal} />
     <Header />
     <div
       className="max-w-2xl w-full bg-blue-100 rounded-xl flex justify-center items-center py-2 mt-3 hover:bg-blue-200 cursor-pointer"
       onClick={openModal}
     >
       <span className="text-blue-500 font-bold text-lg">Upload Image</span>
     </div>
     {loading ? (
       <div className="font-bold text-gray-400 mt-36 text-4xl">Loading...</div>
     ) : (
       <Body />
     )}
   </div>
 );
}
Enter fullscreen mode Exit fullscreen mode

19. Há três componentes referenciados neste código que ainda não existem —Body.tsx,UploadImage.tsx, e Header.tsx.

Crie um novo diretório chamado components, e crie um novo arquivo para cada um deles no diretório de componentes. Em seguida, insira o seguinte código em cada um:

**Body.tsx:

declare let window: any;
import Identicon from "identicon.js";
import React from "react";
import { useData } from "../contexts/DataContext";

const Body = () => {
 const { images } = useData();
 return (
   <>
     {images.length > 0 &&
       images.map((image, index) => (
         <BodyItem
           key={index}
           totalDonationss={image.donationAmount}
           address={image.author}
           description={image.description}
           hash={image.hash}
           id={image.id}
         />
       ))}
   </>
 );
};

export default Body;

const BodyItem = ({ address, description, totalDonationss, hash, id }) => {
 const { donateImageOwner, updateImages } = useData();
 var data = new Identicon(address, 200).toString();
 return (
   <div className="w-full md:mx-0 md:max-w-2xl mt-5 p-3 border rounded-xl flex flex-col">
     <div className="flex flex-row space-x-5 bg-gray-100 rounded-t-xl py-3 px-4 border-t border-l border-r font-mono items-center">
       <img width={35} height={35} src={`data:image/png;base64, ${data}`} />
       <div className="overflow-ellipsis w-52 overflow-hidden">{address}</div>
     </div>
     <img src={`https://ipfs.filebase.io/ipfs/${hash}`} />
     <div className="py-3 px-4 flex flex-col border-l border-r">
       <span className="font-sans font-bold">Description</span>
       <span className="font-sans pt-2">{description}</span>
     </div>
     <div className="bg-gray-100 rounded-b-xl py-3 px-4 border-b border-l border-r font-mono flex flex-row justify-between">
       <span>
         Total DONATIONS: {window.web3.utils.fromWei(totalDonations, "Ether")}{" "}
         MATIC
       </span>
       <div
         onClick={async () => {
           let donationAmount = window.web3.utils.toWei("0.1", "Ether");
           await donationImageOwner(id, donationAmount);
           await updateImages();
         }}
       >
         <span className="cursor-pointer font-bold text-blue-400">
           DONATE: 0.1 MATIC
         </span>
       </div>
     </div>
   </div>
 );
};
Enter fullscreen mode Exit fullscreen mode

**Header.tsx:

import Identicon from "identicon.js";
import React, { useEffect } from "react";
import { useData } from "../../contexts/DataContext";

function Header() {
 const { account } = useData();
 const [data, setData] = React.useState();
 useEffect(() => {
   if (account !== "0x0") {
     setData(new Identicon(account, 200).toString());
   }
 }, [account]);
 return (
   <div className="container items-center">
     <div className="flex flex-col md:flex-row items-center md:justify-between border py-3 px-5 rounded-xl">
       <span className="font-mono">Polygon MATIC</span>
       <div className="flex flex-row space-x-2 items-center">
         <div className="h-5 w-5 rounded-full bg-blue-500"></div>
         <span className="font-mono text-xl font-bold">Decentagram</span>
       </div>
       <div className="flex flex-row space-x-2 items-center">
         <span className="font-mono overflow-ellipsis w-52 overflow-hidden">
           {account}
         </span>
         {account && data && (
           <img
             width={35}
             height={35}
             src={`data:image/png;base64, ${data}`}
           />
         )}
       </div>
     </div>
   </div>
 );
}

export default Header;

UploadImage.tsx:

import { Dialog, Transition } from "@headlessui/react";
import { create } from "ipfs-http-client";
import { Fragment, useState } from "react";
import { useData } from "../contexts/DataContext";

interface Props {
 isOpen: boolean;
 closeModal: () => void;
}

export const UploadImage: React.FC<Props> = ({ isOpen, closeModal }) => {
 const [buttonTxt, setButtonTxt] = useState<string>("Upload");
 const [file, setFile] = useState<File | null>(null);
 const { contract, account, updateImages } = useData();
 const client = create({ url: "https://api.filebase.io/v1/ipfs/" });
 const [description, setDescription] = useState<string>("");

 const uploadImage = async () => {
   setButtonTxt("Uploading to IPFS...");
   const added = await client.add(file);
   setButtonTxt("Creating smart contract...");
   contract.methods
     .uploadImage(added.path, description)
     .send({ from: account })
     .then(async () => {
       await updateImages();
       setFile(null);
       setDescription("");
       setButtonTxt("Upload");
       closeModal();
     })
     .catch(() => {
       closeModal();
     });
 };

 return (
   <>
     <Transition appear show={isOpen} as={Fragment}>
       <Dialog
         as="div"
         className="fixed inset-0 z-10 overflow-y-auto"
         onClose={closeModal}
       >
         <Dialog.Overlay className="fixed inset-0 bg-black opacity-40" />

         <div className="min-h-screen px-4 text-center ">
           <Transition.Child
             as={Fragment}
             enter="ease-out duration-300"
             enterFrom="opacity-0"
             enterTo="opacity-100"
             leave="ease-in duration-200"
             leaveFrom="opacity-100"
             leaveTo="opacity-0"
           >
             <Dialog.Overlay className="fixed inset-0" />
           </Transition.Child>

           {/* This element is to trick the browser into centering the modal contents. */}
           <span
             className="inline-block h-screen align-middle"
             aria-hidden="true"
           >
             &#8203;
           </span>
           <Transition.Child
             as={Fragment}
             enter="ease-out duration-300"
             enterFrom="opacity-0 scale-95"
             enterTo="opacity-100 scale-100"
             leave="ease-in duration-200"
             leaveFrom="opacity-100 scale-100"
             leaveTo="opacity-0 scale-95"
           >
             <div className="inline-block w-full p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white shadow-xl rounded-2xl max-w-xl">
               <Dialog.Title
                 as="h3"
                 className="text-lg font-medium leading-6 text-gray-900"
               >
                 Upload Image to IPFS
               </Dialog.Title>
               <div className="mt-2">
                 <input
                   onChange={(e) => setFile(e.target.files[0])}
                   className="my-3"
                   type="file"
                 />
               </div>

               {file && (
                 <div className="mt-2">
                   <img src={URL.createObjectURL(file)} />
                 </div>
               )}

               <div className="mt-4">
                 <textarea
                   onChange={(e) => {
                     setDescription(e.target.value);
                   }}
                   value={description}
                   placeholder="Description"
                   className="px-3 py-1 font-sourceSansPro text-lg bg-gray-100 hover:bg-white focus:bg-white rounded-lg border-4 hover:border-4 border-transparent hover:border-green-200 focus:border-green-200 outline-none focus:outline-none focus:ring w-full pr-10 transition-all duration-500 ring-transparent"
                 />
               </div>
               <div className="mt-4">
                 <button
                   type="button"
                   disabled={buttonTxt !== "Upload"}
                   className="inline-flex justify-center px-4 py-2 text-sm font-medium text-blue-900 bg-blue-100 border border-transparent rounded-md hover:bg-blue-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:ring-blue-500"
                   onClick={() => {
                     if (file) uploadImage();
                   }}
                 >
                   {buttonTxt}
                 </button>
               </div>
             </div>
           </Transition.Child>
         </div>
       </Dialog>
     </Transition>
   </>
 );
};
Enter fullscreen mode Exit fullscreen mode

20. Agora, para usar o cliente IPFS, vamos instalar o pacote do cliente IPFS HTTP:

npm install ipfs-http-client
Enter fullscreen mode Exit fullscreen mode

21. Por fim, antes de publicarmos este contrato, precisamos configurar a autenticação por meio de nossa carteira criptográfica.

Primeiro, certifique-se de que sua carteira esteja conectada à rede de teste Polygon Mumbai. Para obter instruções sobre como fazer isso, confira este guia aqui.

22. Em seguida, crie um novo arquivo chamado .secretna raiz do seu projeto.

Neste arquivo, insira a frase mnemônica de sua carteira criptográfica ou Frase de recuperação secreta. Observe que esta frase NUNCA deve ser compartilhada com ninguém e deve ser colocada em seu arquivo .gitignore se você planeja publicar este projeto no GitHub.

23. Para transações enviadas, você precisará de algum MATIC na rede de testes, que pode ser obtido na Torneira Polygon Matic.

24. Para cobrir as taxas de gás, usaremos o pacote hdwallet-provider, que pode ser instalado com o comando:

npm install @truffle/hdwallet-provider
Enter fullscreen mode Exit fullscreen mode

25. Agora que temos esses detalhes de configuração, atualize seu arquivo truffle-config.js com o seguinte:

const HDWalletProvider = require("@truffle/hdwallet-provider");
const fs = require("fs");
const mnemonic = fs.readFileSync(".secret").toString().trim();

module.exports = {
 networks: {
   development: {
     host: "localhost",
     port: 7545,
     network_id: "*",
   },
   matic: {
     provider: () =>
       new HDWalletProvider({
         mnemonic: {
           phrase: mnemonic,
         },
         providerOrUrl: `https://matic-mumbai.chainstacklabs.com/`,
         chainId: 80001,
       }),
     network_id: 80001,
     confirmations: 2,
     timeoutBlocks: 200,
     skipDryRun: true,
     chainId: 80001,
   },
 },
 contracts_directory: "./contracts",
 contracts_build_directory: "./abis",
 compilers: {
   solc: {
     optimizer: {
       enabled: true,
       runs: 200,
     },
   },
 },
 db: {
   enabled: false,
 },
};
Enter fullscreen mode Exit fullscreen mode

26. Agora é hora de implantar o contrato inteligente do seu aplicativo de doação na rede Polygon Mumbai com o comando:

truffle migrate --network matic
Enter fullscreen mode Exit fullscreen mode

Uma implantação bem-sucedida produzirá a seguinte saída:

Image description

27. Em seguida, inicie o aplicativo da web com o comando:

npm run build
npm start
Enter fullscreen mode Exit fullscreen mode

28. O aplicativo será executado em localhost:3000. Vai parecer algo assim:

Image description

29. Selecione ‘Upload Image’ para carregar uma imagem para o aplicativo.

Image description

30. Selecione um arquivo do seu computador local, forneça uma descrição opcional e selecione ‘Upload’.

Image description

Quer saber mais sobre o Filebase? Você pode se inscrever para uma Conta gratuita hoje para começar com o IPFS.

Este artigo foi escrito por Jessie Mongeon e traduzido por Diogo Jorge. O artigo original pode ser encontrado aqui.

Top comments (0)