WEB3DEV

Cover image for Como criar uma DAO com Next.js e thirdweb
Banana Labs
Banana Labs

Posted on

Como criar uma DAO com Next.js e thirdweb

capa

Nota do editor: este artigo foi atualizado em 11 de abril de 2022 para se alinhar ao lançamento do thirdweb v2.

O que é uma DAO?

DAO significa Organização Autônoma Descentralizada. Como diz o nome, uma DAO é uma organização sem um líder único; em vez disso, as regras são codificadas na blockchain. Por isso, uma DAO é completamente transparente e todos os membros têm uma participação. Grandes decisões são tomadas por meio de votação entre aqueles que possuem tokens não fungíveis (NFTs) da DAO, que concedem a possibilidade de ser membro.

Hoje, vamos construir nossa própria DAO usando Next.js, thirdweb, MetaMask e Alchemy. Isto permitirá que os usuários cunhem o NFT de sua DAO, recebam criptomoedas por meio de airdrops e participem das pesquisas da DAO. Este tutorial será escrito apenas com JavaScript, então você não precisa conhecer nada de Solidity.

Pré-requisitos

Para entender e acompanhar este tutorial, você deve ter o seguinte:

  • Conhecimento prático de JavaScript, Next.js e blockchain
  • Uma carteira MetaMask
  • Uma conta com Alchemy

Configuração

Começaremos configurando um aplicativo Next.js com o seguinte comando:

npx create-next-app my-dao
Enter fullscreen mode Exit fullscreen mode

Criando Um Aplicativo Com Alchemy

Em seguida, vá para Alchemy, faça login, clique em Criar aplicativo e forneça os detalhes necessários. Certifique-se de usar a mesma cadeia que você usou no thirdweb – no nosso caso, é a cadeia Ethereum e a rede Rinkeby.

Criando Alchemy App

Depois que o aplicativo for criado, copie a chave de API HTTP.

Obtendo a chave privada da carteira

Para cunhar NFTs e executar certos scripts, precisaremos da chave privada da carteira.

Para acessá-la, abra a extensão do navegador MetaMask e clique em Detalhes da conta. Você deve ver sua chave privada aqui; exporte-a e copie-a em algum lugar seguro.

Adicionando variáveis .env

Vamos adicionar essas variáveis em um arquivo .env para podermos acessá-las mais tarde:

PRIVATE_KEY=<wallet_private_key>
ALCHEMY_API_URL=<alchemy_http_key>
WALLET_ADDRESS=<public_wallet_address>
Enter fullscreen mode Exit fullscreen mode

Como não queremos enviá-las para o GitHub, certifique-se de adicioná-las no gitignore

Adicionando a funcionalidade de login usando a MetaMask

Nos DApps, a MetaMask é a carteira mais popular usada, então adicionaremos o login da MetaMask com o thirdweb.

Vamos precisar de dois pacotes para instalar:

npm i @thirdweb-dev/react @thirdweb-dev/sdk ethers # npm

yarn add @thirdweb-dev/react @thirdweb-dev/sdk ethers # yarn
Enter fullscreen mode Exit fullscreen mode

Adicionando o provedor thirdweb

Precisamos envolver todo o nosso aplicativo no provedor thirdweb para acessar os detalhes de login e outras informações necessárias para os componentes:

import "../styles/globals.css";
import {ThirdwebProvider } from "@thirdweb-dev/react";

function MyApp({ Component, pageProps }) {
  return (
    <ThirdwebProvider desiredChainId={activeChainId}>
      <Component {...pageProps} />
    </ThirdwebProvider>
  );
}
export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Para fins de autenticação, também precisamos especificar o tipo de autenticação e os chain IDs suportados. Estamos usando MetaMask e a cadeia Rinkeby, então adicione o seguinte também:

import { ChainId, ThirdwebProvider } from "@thirdweb-dev/react";
const activeChainId = ChainId.Rinkeby;
Enter fullscreen mode Exit fullscreen mode

Por fim, passe-os como adereços no provedor assim:

<ThirdwebProvider desiredChainId={activeChainId}>
      <Component {...pageProps} />
  </ThirdwebProvider>
Enter fullscreen mode Exit fullscreen mode

Adicionando o componente de login

Crie uma nova pasta chamada components na raiz do projeto e adicione um arquivo Login.js a ela:

import { useMetamask } from "@thirdweb-dev/react";
const Login = () => {
  const connectWithMetamask = useMetamask();
  return (
    <div>
      <button onClick={connectWithMetamask}>Sign in using MetaMask</button>
    </div>
  );
};
export default Login;
Enter fullscreen mode Exit fullscreen mode

Felizmente, o thirdweb fornece uma função connectWallet que podemos usar para adicionar autenticação!

Renderizando o componente de login

Dentro do index.js, renderize a tela de login se não houver endereço (se o usuário não estiver logado):

const address = useAddress();
if (!address) {
  return ;
}
Enter fullscreen mode Exit fullscreen mode

Isso permitirá que nossos usuários façam login, mas depois ele mostra apenas uma tela em branco. Então, no outro bloco de retorno, vamos mostrar ao usuário seu endereço:

export default function Home() {
  const { address } = useWeb3();
  if (!address) {
    return <Login />;
  }
  return (
    <div className={styles.container}>
      <h2>You are signed in as {address}</h2>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

O login funciona, mas não parece bom agora. Então, crie um novo arquivo Login.module.css na pasta styles e adicione o seguinte:

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}
Enter fullscreen mode Exit fullscreen mode

Em seguida, adicione as seguintes classes ao Login.js:

<div className={styles.container}>
  <button className={styles.button} onClick={() => connectWallet("injected")}>
    Sign in using MetaMask
  </button>
</div>
Enter fullscreen mode Exit fullscreen mode

E, finalmente, importe os estilos:

import styles from "../styles/Login.module.css";
Enter fullscreen mode Exit fullscreen mode

Isso nos dará uma tela de login simples, mas de boa aparência.

Metamask Acesso

Inicializando o SDK thirdweb

Agora precisamos inicializar o thirdweb SDK para os vários scripts que vamos executar. Comece criando uma nova pasta chamada scripts com um arquivo initialize-sdk.js dentro dela.

Adicione o seguinte código ao arquivo:

import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import ethers from "ethers";
import dotenv from "dotenv";
dotenv.config();
if (!process.env.PRIVATE_KEY || process.env.PRIVATE_KEY === "") {
  console.log("🛑 Private key not found.");
}
if (!process.env.ALCHEMY_API_URL || process.env.ALCHEMY_API_URL === "") {
  console.log("🛑 Alchemy API URL not found.");
}
if (!process.env.WALLET_ADDRESS || process.env.WALLET_ADDRESS === "") {
  console.log("🛑 Wallet Address not found.");
}
const sdk = new ThirdwebSDK(
  new ethers.Wallet(
    process.env.PRIVATE_KEY,
    ethers.getDefaultProvider(process.env.ALCHEMY_API_URL)
  )
);
(async () => {
  try {
    const address = await sdk.getSigner().getAddress();
    console.log("SDK initialized by address:", address);
  } catch (err) {
    console.error("Failed to get the address", err);
    process.exit(1);
  }
})();
export default sdk;
Enter fullscreen mode Exit fullscreen mode

Isso inicializará o thirdweb SDK e, como você pode ver, precisamos instalar alguns pacotes:

npm i dotenv # npm

yarn add dotenv # yarn
Enter fullscreen mode Exit fullscreen mode

Estamos usando importações modulares aqui, então crie um novo arquivo package.json dentro da pasta de scripts e simplesmente adicione o seguinte:

{
  "name": "scripts",
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

Por fim, execute o script:

node scripts/initialize-sdk.js
Enter fullscreen mode Exit fullscreen mode

O script pode levar algum tempo para ser executado, mas depois de algum tempo você obterá o endereço do seu aplicativo.

thirdweb address

Vamos precisar disso nas próximas etapas, então guarde-o em algum lugar seguro.

Adicionando recursos para cunhar uma NFT

Para esta etapa, vamos precisar de algum ETH de teste, então vá para uma torneira (faucet) como esta e pegue um pouco.

Criando e configurando um NFT

Crie um novo arquivo chamado deploy-drop.js dentro da pasta de scripts. Aqui, adicione o seguinte script:

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";

(async () => {
  try {
    const editionDropAddress = await sdk.deployer.deployEditionDrop({
      name: "LogRocket DAO", // Nome da Coleção NFT para a DAO
      description: "A DAO for all the LogRocket readers.", // Description
      image: "image_Address", // PFP para a Coleção NFT 
      primary_sale_recipient: ethers.constants.AddressZero,
    });
    const editionDrop = sdk.getEditionDrop(editionDropAddress);
    const metadata = await editionDrop.metadata.get();
    console.log(
      "✅ Successfully deployed editionDrop contract, address:",
      editionDropAddress
    );
    console.log("✅ editionDrop metadata:", metadata);
  } catch (error) {
    console.log("failed to deploy editionDrop contract", error);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Você precisará atualizar algumas coisas aqui:

  • Atualize o endereço do aplicativo com o novo endereço do aplicativo que você obteve executando o script anterior
  • Atualize o nome do lançamento NFT para a DAO e sua descrição
  • Adicione uma imagem para o lançamento NFT criando uma nova pasta chamada assets e adicionando a imagem para seu NFT lá

Depois de atualizar os detalhes, execute o seguinte script:

node scripts/deploy-drop.js
Enter fullscreen mode Exit fullscreen mode

Aguarde a execução do script e você deverá obter um endereço e os metadados.

NFT Endereço e Metadados

Isso criará uma nova edição do contrato de lançamento para nós! Você pode até conferir a transação no Rinkeby Etherscan.

Vamos configurar nosso NFT agora! Crie um novo arquivo config-nft.js dentro da pasta de scripts e adicione o seguinte:

import sdk from "./initialize-sdk.js";

const editionDrop = sdk.getEditionDrop("EDITION_DROP_ADDDRESS");

(async () => {
  try {
    await editionDrop.createBatch([
      {
        name: "LogRocket DAO", // Nome da Coleção NFT para a DAO
        description: "A DAO for all the LogRocket readers.", // Description
        image: "image_address", // Imagem para o NFT
      },
    ]);
    console.log("✅ Successfully created a new NFT in the drop!");
  } catch (error) {
    console.error("failed to create the new NFT", error);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Você precisa atualizar o endereço do pacote de lançamento e os detalhes no objeto dentro de createBatch. Esses detalhes serão usados para o NFT!

Depois de atualizar todos eles, execute o seguinte script:

node scripts/config-nft.js
Enter fullscreen mode Exit fullscreen mode

Deve fornecer uma saída como esta.

NFT Saída

Se você vir o módulo no painel de controle da thirdweb, verá que um NFT foi criado!🥳

Painel da thirdweb

Por fim, vamos adicionar uma condição de reivindicação ao nosso NFT.

Definir uma condição de reivindicação nos permitirá definir um limite para os NFTs e permitir um limite máximo específico por transação. Vamos definir uma condição de reivindicação a partir do próprio painel, então clique no botão Configurações e crie uma nova fase de reivindicação.

Configurações

Após concluir a atualização, clique em Atualizar fase de reivindicação e confirme a pequena transação.

Verificando se o usuário tem um NFT

Antes de criar um botão mint que permita aos usuários cunhar NFTs, vamos verificar se o usuário já possui um NFT. Não queremos que os usuários criem vários NFTs!

Comece adicionando duas novas variáveis, sdk e bundleDropModule, dessa forma antes do nosso componente funcional:

const editionDrop = useEditionDrop(
    "0x2f66A5A2BCB272FFC9EB873E3482A539BEB6f02a"
  );
Enter fullscreen mode Exit fullscreen mode

Você também precisará importar useEditionDrop:

import { useAddress, useEditionDrop } from "@thirdweb-dev/react";
Enter fullscreen mode Exit fullscreen mode

Agora, vamos criar um estado para hasClamedNFT:

const [hasClaimedNFT, setHasClaimedNFT] = useState(false);
Enter fullscreen mode Exit fullscreen mode

Também precisamos criar um Hook useEffect para verificar se o usuário possui o NFT:

useEffect(() => {
    if (!address) {
      return;
    }
    const checkBalance = async () => {
      try {
        const balance = await editionDrop.balanceOf(address, 0);
        if (balance.gt(0)) {
          setHasClaimedNFT(true);
          console.log("🎉 You have an NFT!");
        } else {
          setHasClaimedNFT(false);
          console.log("🤷‍♂️ You don't have an NFT.");
        }
      } catch (error) {
        setHasClaimedNFT(false);
        console.error("Failed to get nft balance", error);
      }
    };
    checkBalance();
  }, [address, editionDrop]);
Enter fullscreen mode Exit fullscreen mode

Primeiramente, ele verificará se o usuário está conectado. Se o usuário não estiver conectado, não retornará nada. Então, isso verifica se o usuário tem o NFT com o token ID 0 no contrato de lançamento que importamos no topo.

Se você abrir o console no site, deve mostrar que você não possui um NFT.

Mensagem NFT

Criando um botão para cunhar NFTs

Vamos criar o botão para cunhar NFTs! Crie uma nova função chamada mintNft assim:

const mintNft = async () => {
  setIsClaiming(true);
  try {
    await bundleDropModule.claim("0", 1);
    setHasClaimedNFT(true);
    console.log("🌊 Successfully Minted the NFT!");
  } catch (error) {
    console.error("failed to claim", error);
  } finally {
    setIsClaiming(false);
  }
};
Enter fullscreen mode Exit fullscreen mode

Chamaremos esta função quando um botão for clicado para cunhar o NFT na carteira do usuário. Mas primeiro, vamos adicionar o estado isClaiming:

const [isClaiming, setIsClaiming] = useState(false);
Enter fullscreen mode Exit fullscreen mode

Vamos criar o botão agora! Dentro do bloco de retorno final, adicione o seguinte:

<div>
  <h1>Mint your free LogRocket DAO Membership NFT 💳</h1>
  <button disabled={isClaiming} onClick={() => mintNft()}>
    {isClaiming ? "Minting..." : "Mint your nft (FREE)"}
  </button>
</div>
Enter fullscreen mode Exit fullscreen mode

Agora, depois de entrarmos, ele deve nos mostrar uma tela como esta.

Tela do botão NFT

Se você tentar o botão Mint your nft (FREE), ele deve aparecer na tela MetaMask para concluir a transação. No console, você deve ver o seguinte.

Mensagem NFT

Por fim, logo acima do bloco de retorno final, adicione esta verificação para ver se o usuário já reivindicou o NFT:

if (hasClaimedNFT) {
  return (
    <div>
      <h1>You have the DAO Membership NFT!</h1>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Adicionando estilos

Concluímos a construção da funcionalidade de cunhagem NFT, mas parece feia, então vamos adicionar alguns estilos básicos. Dentro de Home.module.css, adicione o seguinte:

.container {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  background-color: #7449bb;
}
.container > h1 {
  font-size: 3rem;
  color: #fff;
  font-weight: bold;
}
.button {
  color: #7449bb;
  background-color: white;
  border: none;
  border-radius: 5px;
  padding: 10px;
  font-size: 16px;
  margin: 4px 2px;
  cursor: pointer;
  font-weight: 500;
}
Enter fullscreen mode Exit fullscreen mode

Também precisamos adicionar os classNames:

if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
        <h1>You have the DAO Membership NFT!</h1>
      </div>
    );
  }
  return (
    <div className={styles.container}>
      <h1>Mint your free LogRocket DAO Membership NFT 💳</h1>
      <button
        className={styles.button}
        disabled={isClaiming}
        onClick={() => mintNft()}
      >
        {isClaiming ? "Minting..." : "Mint your NFT (FREE)"}
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

Isso nos dá uma melhor tela de cunhagem.

Tela NFT Estilizada

Criando e implantando um token de governança

Crie um novo arquivo chamado deploy-token.js na pasta de scripts. Adicione o seguinte a ele:

import { AddressZero } from "@ethersproject/constants";
import sdk from "./initialize-sdk.js";
(async () => {
  try {
    const tokenAddress = await sdk.deployer.deployToken({
      name: "LogRocket Token", // name of the token
      symbol: "LR", // symbol
      primary_sale_recipient: AddressZero, // 0x0000000000000000000000000000000000000000
    });
    console.log(
      "✅ Successfully deployed token module, address:",
      tokenAddress
    );
  } catch (error) {
    console.error("failed to deploy token module", error);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Este script criará um novo módulo de token com um nome e símbolo. Você precisará atualizar manualmente o endereço do aplicativo, o nome do token e o símbolo.

Após a atualização, execute o script.

Você pode verificar esse token pelo endereço no Rinkeby Etherscan e também adicioná-lo à sua carteira MetaMask clicando em Importar tokens.

Após a importação, você deverá ver o token em seus ativos.

token

Atualmente é zero, então vamos cunhar alguns tokens!

Cunhando os tokens

Crie um novo arquivo chamado mint-token.js dentro da pasta de scripts e adicione o seguinte:

import { ethers } from "ethers";
import sdk from "./initialize-sdk.js";
const tokenModule = sdk.getTokenModule(
  "TOKEN_MODULE_ADDRESS"
);

(async () => {
  try {
    const amount = 1_000_000;
    const amountWith18Decimals = ethers.utils.parseUnits(amount.toString(), 18);
    await tokenModule.mint(amountWith18Decimals);
    const totalSupply = await tokenModule.totalSupply();
    console.log(
      "✅ There now is",
      ethers.utils.formatUnits(totalSupply, 18),
      "$LR in circulation"
    );
  } catch (error) {
    console.error("Failed to mint tokens", error);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Atualize o endereço do módulo de token com o endereço obtido no último script e você pode atualizar a quantidade que deseja cunhar.

Depois de estar pronto para cunhar, execute o script:

node scripts/mint-token.js
Enter fullscreen mode Exit fullscreen mode

Agora você deve ver a quantidade de tokens que cunhou em sua carteira MetaMask!

1 Milhão token

Tokens de lançamento aéreo (Airdrop)

Podemos querer enviar os tokens para nossos detentores de NFT, então vamos criar um script para isso. Crie um novo arquivo airdrop.js dentro dos scripts e adicione o seguinte:

import sdk from "./initialize-sdk.js";
const editionDrop = sdk.getEditionDrop(
  "EDITION_ADDRESS"
);
const token = sdk.getToken("TOKEN_ADDRESS");
(async () => {
  try {
    const walletAddresses = await editionDrop.history.getAllClaimerAddresses(0);
    if (walletAddresses.length === 0) {
      console.log(
        "No NFTs have been claimed yet, ask yourfriends to claim some free NFTs!"
      );
      process.exit(0);
    }
    const airdropTargets = walletAddresses.map((address) => {
      const randomAmount = Math.floor(
        Math.random() * (10000 - 1000 + 1) + 1000
      );
      console.log("✅ Going to airdrop", randomAmount, "tokens to", address);
      const airdropTarget = {
        toAddress: address,
        amount: randomAmount,
      };
      return airdropTarget;
    });
    console.log("🌈 Starting airdrop...");
    await token.transferBatch(airdropTargets);
    console.log(
      "✅ Successfully airdropped tokens to all the holders of the NFT!"
    );
  } catch (err) {
    console.error("Failed to airdrop tokens", err);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Depois de executar o script, você deve obter algo assim.

Mensagem de Airdrop

Atualmente, apenas você cunhou um NFT, portanto, ele não enviará o token para outra pessoa. Mas isso pode ser usado para enviá-lo para outros detentores de NFT posteriormente.

Permitindo que os usuários votem

Crie um novo arquivo deploy-vote.js na pasta de scripts e adicione o seguinte:

import sdk from "./initialize-sdk.js";
(async () => {
  try {
    const voteContractAddress = await sdk.deployer.deployVote({
      name: "LR Dao's Proposals",
      voting_token_address: "TOKEN_ADDRESS",
      voting_delay_in_blocks: 0,
      voting_period_in_blocks: 6570,
      voting_quorum_fraction: 0,
      proposal_token_threshold: 0,
    });
    console.log(
      "✅ Successfully deployed vote contract, address:",
      voteContractAddress
    );
  } catch (err) {
    console.error("Failed to deploy vote contract", err);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Atualize o endereço do aplicativo, o nome e o endereço do token de votação e execute o script:

node scripts/deploy-vote.js
Enter fullscreen mode Exit fullscreen mode

Também precisamos configurar um módulo de votação, então crie um novo script chamado setup-vote.js e adicione o seguinte:

import sdk from "./initialize-sdk.js";
const vote = sdk.getVote("VOTE_ADDRESS");
const token = sdk.getToken("TOKEN_ADDRESS");

(async () => {
  try {
    await token.roles.grant("minter", vote.getAddress());
    console.log(
      "Successfully gave vote contract permissions to act on token contract"
    );
  } catch (error) {
    console.error(
      "failed to grant vote contract permissions on token contract",
      error
    );
    process.exit(1);
  }
  try {
    const ownedTokenBalance = await token.balanceOf(process.env.WALLET_ADDRESS);
    const ownedAmount = ownedTokenBalance.displayValue;
    const percent90 = (Number(ownedAmount) / 100) * 90;
    await token.transfer(vote.getAddress(), percent90);
    console.log(
      "✅ Successfully transferred " + percent90 + " tokens to vote contract"
    );
  } catch (err) {
    console.error("failed to transfer tokens to vote contract", err);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Você precisará executar este script para concluí-lo:

node scripts/setup-vote.js

Enter fullscreen mode Exit fullscreen mode

Agora que temos nosso módulo de votação pronto, vamos criar algumas propostas!

Crie um novo arquivo chamado vote-proposals.js dentro da pasta scripts e adicione o seguinte:

import sdk from "./initialize-sdk.js";
import { ethers } from "ethers";
const vote = sdk.getVote("0x31c5840b31A1F97745bDCbB1E46954b686828E0F");
const token = sdk.getToken("0x6eefd78C9C73505AA71A13FeE31D9718775c9086");
(async () => {
  try {
    const amount = 420_000;
    const description =
      "Should the DAO mint an additional " +
      amount +
      " tokens into the treasury?";
    const executions = [
      {
        toAddress: token.getAddress(),
        nativeTokenValue: 0,
        transactionData: token.encoder.encode("mintTo", [
          vote.getAddress(),
          ethers.utils.parseUnits(amount.toString(), 18),
        ]),
      },
    ];
    await vote.propose(description, executions);
    console.log("✅ Successfully created proposal to mint tokens");
  } catch (error) {
    console.error("failed to create first proposal", error);
    process.exit(1);
  }
})();
Enter fullscreen mode Exit fullscreen mode

Você precisa atualizar os endereços dos módulos e, se quiser atualizar a mensagem da proposta, também pode atualizar.
Por fim, execute o script. Deve mostrar algo assim.

mensagem

Se você verificar agora o painel de controle do thirdweb, a proposta foi criada. 🎉

painel do thirdweb

Exibição de propostas no site

Primeiro, importe o token e o módulo de voto:

const token = useToken("TOKEN_ADDRESS");
const vote = useVote("VOTE_ADDRESS");
Enter fullscreen mode Exit fullscreen mode

Vamos precisar de três useStates, assim:

const [proposals, setProposals] = useState([]);
const [isVoting, setIsVoting] = useState(false);
const [hasVoted, setHasVoted] = useState(false); 
Enter fullscreen mode Exit fullscreen mode

Obtendo as propostas

Precisamos obter as propostas para exibi-las na tela, então crie este useEffect:

useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    const getAllProposals = async () => {
      try {
        const proposals = await vote.getAll();
        setProposals(proposals);
        console.log("📋 Proposals:", proposals);
      } catch (error) {
        console.log("failed to get proposals", error);
      }
    };
    getAllProposals();
  }, [hasClaimedNFT, vote]);
Enter fullscreen mode Exit fullscreen mode

Em seguida, crie uma nova função handleFormSubmit:

const handleFormSubmit = async (e) => {
    e.preventDefault();
    e.stopPropagation();
    setIsVoting(true);
    const votes = proposals.map((proposal) => {
      const voteResult = {
        proposalId: proposal.proposalId,
        vote: 2,
      };
      proposal.votes.forEach((vote) => {
        const elem = document.getElementById(
          proposal.proposalId + "-" + vote.type
        );
        if (elem.checked) {
          voteResult.vote = vote.type;
          return;
        }
      });
      return voteResult;
    });
    try {
      const delegation = await token.getDelegationOf(address);
      if (delegation === AddressZero) {
        await token.delegateTo(address);
      }
      try {
        await Promise.all(
          votes.map(async ({ proposalId, vote: _vote }) => {
            const proposal = await vote.get(proposalId);
            if (proposal.state === 1) {
              return vote.vote(proposalId, _vote);
            }
            return;
          })
        );
        try {
          await Promise.all(  votes.map(async ({ proposalId, vote: _vote }) => {
            const proposal = await vote.get(proposalId);
            if (proposal.state === 1) {
              return vote.vote(proposalId, _vote);
            }
            return;
          })
        );
        try {
          await Promise.all(
            votes.map(async ({ proposalId }) => {
              const proposal = await vote.get(proposalId);
              if (proposal.state === 4) {
                return vote.execute(proposalId);
              }
            })
          );
          setHasVoted(true);
          console.log("successfully voted");
        } catch (err) {
          console.error("failed to execute votes", err);
        }
      } catch (err) {
        console.error("failed to vote", err);
      }
    } catch (err) {
      console.error("failed to delegate tokens");
    } finally {
      setIsVoting(false);
    }
  };
Enter fullscreen mode Exit fullscreen mode

Esta função vai coletar o voto.

Renderizando as propostas

Substitua o bloco if (hasClamedNFT) por este:

if (hasClaimedNFT) {
    return (
      <div className={styles.container}>
          <h2>Active Proposals</h2>
          <form onSubmit={handleFormSubmit}>
            {proposals.map((proposal) => (
              <Proposal
                key={proposal.proposalId}
                votes={proposal.votes}
                description={proposal.description}
                proposalId={proposal.proposalId}
              />
            ))}
            <button
              onClick={handleFormSubmit}
              type="submit"
              className={styles.button}
            >
              {isVoting
                ? "Voting..."
                "Submit Votes"}
            </button>
          </form>
        </div>
    );
  }
Enter fullscreen mode Exit fullscreen mode

Estamos criando um componente separado para a proposta para manter as coisas limpas. Então, crie um novo arquivo chamado Proposal.js na pasta components e adicione o seguinte:

import styles from "../styles/Proposal.module.css";

const Proposal = ({ description, votes, proposalId }) => {
  return (
    <div className={styles.proposal}>
      <h5 className={styles.description}>{description}</h5>
      <div className={styles.options}>
        {votes.map((vote) => (
          <div key={vote.type}>
            <input
              type="radio"
              id={proposalId + "-" + vote.type}
              name={proposalId}
              value={vote.type}
              defaultChecked={vote.type === 2}
            />
            <label htmlFor={proposalId + "-" + vote.type}>{vote.label}</label>
          </div>
        ))}
      </div>
    </div>
  );
};
export default Proposal;
Enter fullscreen mode Exit fullscreen mode

Eu também adicionei um estilo básico, então crie um novo arquivo Proposal.module.css na pasta de estilos:

.proposal {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background-color: #fafafa;
  border-radius: 10px;
  box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);
  padding: 20px;
  margin: 20px;
}
.options {
  display: flex;
  flex-direction: column;
  justify-content: space-between;
  align-items: flex-start;
  width: 100%;
  margin-top: 1rem;
}
Enter fullscreen mode Exit fullscreen mode

Para centralizar os itens, também adicionei os seguintes estilos em Home.module.css:

.container > form {
  display: flex;
  flex-direction: column;
  align-items: center;
}
Enter fullscreen mode Exit fullscreen mode

Você chegará a esta tela onde poderá enviar seus votos. 🎉

Tela de Votos

Por fim, vamos fazer uma função para verificar se a pessoa já votou.

Primeiro, crie um novo useEffect:

useEffect(() => {
    if (!hasClaimedNFT) {
      return;
    }
    if (!proposals.length) {
      return;
    }
    const checkIfUserHasVoted = async () => {
      try {
        const hasVoted = await vote.hasVoted(proposals[0].proposalId, address);
        setHasVoted(hasVoted);
      } catch (error) {
        console.error("Failed to check if wallet has voted", error);
      }
    };
    checkIfUserHasVoted();
  }, [hasClaimedNFT, proposals, address, vote]);
Enter fullscreen mode Exit fullscreen mode

E substitua o botão por este:

<button
  onClick={handleFormSubmit}
  type="submit"
  disabled={isVoting || hasVoted}
  className={styles.button}
>
  {isVoting ? "Voting..." : hasVoted ? "You Already Voted" : "Submit Votes"}
</button>
Enter fullscreen mode Exit fullscreen mode

Depois de votar, ele deve mostrar a mensagem You Already Voted:

Mensagem

Conclusão

Isto foi tudo para este tutorial, espero que tenham gostado e possam usá-lo para fazer sua própria DAO! Você sempre pode atualizar a DAO e adicionar mais recursos, se desejar.✌️

Você pode encontrar o repositório GitHub para este projeto aqui.


Esse artigo é uma tradução de Avneesh Agarwal feita por @bananlabs. Você pode encontrar o artigo original aqui

Top comments (0)