Created
July 26, 2022 12:16
-
-
Save danicuki/ed841dfdea802b3bb9d887d3be59c67c to your computer and use it in GitHub Desktop.
JS DAO App.jsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { useAddress, useMetamask, useEditionDrop, useToken, useVote } from '@thirdweb-dev/react' | |
import { useState, useEffect, useMemo } from 'react' | |
import { AddressZero } from "@ethersproject/constants"; | |
const App = () => { | |
// Use o hook connectWallet que o thirdweb nos dá. | |
const address = useAddress() | |
const connectWithMetamask = useMetamask() | |
console.log("👋 Address:", address) | |
// inicializar o contrato editionDrop | |
const editionDrop = useEditionDrop("ENDERECO_DO_EDITION_DROP") | |
const token = useToken("ENDERECO_DO_TOKEN") | |
const vote = useVote("ENDERECO_DO_VOTO") | |
// Variável de estado para sabermos se o usuário tem nosso NFT. | |
const [hasClaimedNFT, setHasClaimedNFT] = useState(false) | |
const [isClaiming, setIsClaiming] = useState(false) | |
// Guarda a quantidade de tokens que cada membro tem nessa variável de estado. | |
const [memberTokenAmounts, setMemberTokenAmounts] = useState([]) | |
// O array guardando todos os endereços dos nosso membros. | |
const [memberAddresses, setMemberAddresses] = useState([]) | |
// Uma função para diminuir o endereço da carteira de alguém, não é necessário mostrar a coisa toda. | |
const shortenAddress = (str) => { | |
return str.substring(0, 6) + "..." + str.substring(str.length - 4) | |
} | |
const [proposals, setProposals] = useState([]) | |
const [isVoting, setIsVoting] = useState(false) | |
const [hasVoted, setHasVoted] = useState(false) | |
// Recupere todas as propostas existentes no contrato. | |
useEffect(() => { | |
if (!hasClaimedNFT) { | |
return | |
} | |
// Uma chamada simples para voteModule.getAll() para pegar as propostas. | |
const getAllProposals = async () => { | |
try { | |
const proposals = await vote.getAll() | |
setProposals(proposals) | |
console.log("🌈 Propostas:", proposals) | |
} catch (error) { | |
console.log("falha ao buscar propostas", error) | |
} | |
} | |
getAllProposals() | |
}, [hasClaimedNFT, vote]) | |
// Nós também precisamos checar se o usuário já votou. | |
useEffect(() => { | |
if (!hasClaimedNFT) { | |
return | |
} | |
// Se nós não tivermos terminado de recuperar as propostas do useEffect acima | |
// então ainda nao podemos checar se o usuário votou! | |
if (!proposals.length) { | |
return | |
} | |
const checkIfUserHasVoted = async () => { | |
try { | |
const hasVoted = await vote.hasVoted(proposals[0].proposalId, address) | |
setHasVoted(hasVoted) | |
if (hasVoted) { | |
console.log("🥵 Usuário já votou") | |
} else { | |
console.log("🙂 Usuário ainda não votou") | |
} | |
} catch (error) { | |
console.error("Falha ao verificar se carteira já votou", error) | |
} | |
} | |
checkIfUserHasVoted() | |
}, [hasClaimedNFT, proposals, address, vote]) | |
// Esse useEffect pega todos os endereços dos nosso membros detendo nosso NFT. | |
useEffect(() => { | |
if (!hasClaimedNFT) { | |
return | |
} | |
// Do mesmo jeito que fizemos no arquivo 7-airdrop-token.js! Pegue os usuários que tem nosso NFT | |
// com o tokenId 0. | |
const getAllAddresses = async () => { | |
try { | |
const memberAddresses = await editionDrop.history.getAllClaimerAddresses(0) | |
setMemberAddresses(memberAddresses) | |
console.log("🚀 Endereços de membros", memberAddresses) | |
} catch (error) { | |
console.error("falha ao pegar lista de membros", error) | |
} | |
} | |
getAllAddresses() | |
}, [hasClaimedNFT, editionDrop.history]) | |
// Esse useEffect pega o # de tokens que cada membro tem. | |
useEffect(() => { | |
if (!hasClaimedNFT) { | |
return | |
} | |
// Pega todos os saldos. | |
const getAllBalances = async () => { | |
try { | |
const amounts = await token.history.getAllHolderBalances() | |
setMemberTokenAmounts(amounts) | |
console.log("👜 Quantidades", amounts) | |
} catch (error) { | |
console.error("falha ao buscar o saldo dos membros", error) | |
} | |
} | |
getAllBalances() | |
}, [hasClaimedNFT, token.history]) | |
// Agora, nós combinamos os memberAddresses e os memberTokenAmounts em um único array | |
const memberList = useMemo(() => { | |
return memberAddresses.map((address) => { | |
// Se o endereço não está no memberTokenAmounts, isso significa que eles não | |
// detêm nada do nosso token. | |
const member = memberTokenAmounts?.find(({ holder }) => holder === address) | |
return { | |
address, | |
tokenAmount: member?.balance.displayValue || "0", | |
} | |
}) | |
}, [memberAddresses, memberTokenAmounts]) | |
useEffect(() => { | |
// Se ele não tiver uma carteira conectada, saia! | |
if (!address) { | |
return | |
} | |
const checkBalance = async () => { | |
try { | |
const balance = await editionDrop.balanceOf(address, 0) | |
// Se o saldo for maior do que 0, ele tem nosso NFT! | |
if (balance.gt(0)) { | |
setHasClaimedNFT(true) | |
console.log("🌟 esse usuário tem o NFT de membro!") | |
} else { | |
setHasClaimedNFT(false) | |
console.log("😭 esse usuário NÃO tem o NFT de membro.") | |
} | |
} catch (error) { | |
setHasClaimedNFT(false) | |
console.error("Falha ao ler saldo", error) | |
} | |
} | |
checkBalance() | |
}, [address, editionDrop]) | |
const mintNft = async () => { | |
try { | |
setIsClaiming(true) | |
await editionDrop.claim("0", 1) | |
console.log(`🌊 Cunhado com sucesso! Olhe na OpenSea: https://testnets.opensea.io/assets/${editionDrop.getAddress()}/0`) | |
setHasClaimedNFT(true) | |
} catch (error) { | |
setHasClaimedNFT(false) | |
console.error("Falha ao cunhar NFT", error) | |
} finally { | |
setIsClaiming(false) | |
} | |
} | |
// Esse é o caso em que o usuário ainda não conectou sua carteira | |
// ao nosso webapp. Deixe ele chamar connectWallet. | |
if (!address) { | |
return ( | |
<div className="landing"> | |
<h1>Bem-vind@s à MTBDAO a DAO dos pedaleiros de montanha</h1> | |
<button onClick={connectWithMetamask} className="btn-hero"> | |
Conecte sua carteira | |
</button> | |
</div> | |
) | |
} | |
// Adicione esse pedacinho! | |
if (hasClaimedNFT) { | |
return ( | |
<div className="member-page"> | |
<h1>🚴 Página dos membros da DAO</h1> | |
<p>Parabéns por fazer parte desse clube de bikers!</p> | |
<div> | |
<div> | |
<h2>Lista de Membros</h2> | |
<table className="card"> | |
<thead> | |
<tr> | |
<th>Endereço</th> | |
<th>Quantidade de Tokens</th> | |
</tr> | |
</thead> | |
<tbody> | |
{memberList.map((member) => { | |
return ( | |
<tr key={member.address}> | |
<td>{shortenAddress(member.address)}</td> | |
<td>{member.tokenAmount}</td> | |
</tr> | |
) | |
})} | |
</tbody> | |
</table> | |
</div> | |
<div> | |
<h2>Propostas Ativas</h2> | |
<form | |
onSubmit={async (e) => { | |
e.preventDefault() | |
e.stopPropagation() | |
//antes de fazer as coisas async, desabilitamos o botão para previnir duplo clique | |
setIsVoting(true) | |
// pega os votos no formulário | |
const votes = proposals.map((proposal) => { | |
const voteResult = { | |
proposalId: proposal.proposalId, | |
//abstenção é a escolha padrão | |
vote: 2, | |
} | |
proposal.votes.forEach((vote) => { | |
const elem = document.getElementById( | |
proposal.proposalId + "-" + vote.type | |
) | |
if (elem.checked) { | |
voteResult.vote = vote.type | |
return | |
} | |
}) | |
return voteResult | |
}) | |
// certificamos que o usuário delega seus tokens para o voto | |
try { | |
//verifica se a carteira precisa delegar os tokens antes de votar | |
const delegation = await token.getDelegationOf(address) | |
// se a delegação é o endereço 0x0 significa que eles não delegaram seus tokens de governança ainda | |
if (delegation === AddressZero) { | |
//se não delegaram ainda, teremos que delegar eles antes de votar | |
await token.delegateTo(address) | |
} | |
// então precisamos votar nas propostas | |
try { | |
await Promise.all( | |
votes.map(async ({ proposalId, vote: _vote }) => { | |
// antes de votar, precisamos saber se a proposta está aberta para votação | |
// pegamos o último estado da proposta | |
const proposal = await vote.get(proposalId) | |
// verifica se a proposta está aberta para votação (state === 1 significa está aberta) | |
if (proposal.state === 1) { | |
// se está aberta, então vota nela | |
return vote.vote(proposalId, _vote) | |
} | |
// se a proposta não está aberta, returna vazio e continua | |
return | |
}) | |
) | |
try { | |
// se alguma proposta está pronta para ser executada, fazemos isso | |
// a proposta está pronta para ser executada se o estado é igual a 4 | |
await Promise.all( | |
votes.map(async ({ proposalId }) => { | |
// primeiro pegamos o estado da proposta novamente, dado que podemos ter acabado de votar | |
const proposal = await vote.get(proposalId) | |
//se o estado é igual a 4 (pronta para ser executada), executamos a proposta | |
if (proposal.state === 4) { | |
return vote.execute(proposalId) | |
} | |
}) | |
) | |
// se chegamos aqui, significa que votou com sucesso, então definimos "hasVoted" como true | |
setHasVoted(true) | |
console.log("votado com sucesso") | |
} catch (err) { | |
console.error("falha ao executar votos", err) | |
} | |
} catch (err) { | |
console.error("falha ao votar", err) | |
} | |
} catch (err) { | |
console.error("falha ao delegar tokens") | |
} finally { | |
// de qualquer modo, volta isVoting para false para habilitar o botão novamente | |
setIsVoting(false) | |
} | |
}} | |
> | |
{proposals.map((proposal) => ( | |
<div key={proposal.proposalId} className="card"> | |
<h5>{proposal.description}</h5> | |
<div> | |
{proposal.votes.map(({ type, label }) => { | |
const translations = { | |
Against: "Contra", | |
For: "A favor", | |
Abstain: "Abstenção", | |
} | |
return ( | |
<div key={type}> | |
<input | |
type="radio" | |
id={proposal.proposalId + "-" + type} | |
name={proposal.proposalId} | |
value={type} | |
//valor padrão "abster" vem habilitado | |
defaultChecked={type === 2} | |
/> | |
<label htmlFor={proposal.proposalId + "-" + type}> | |
{translations[label]} | |
</label> | |
</div> | |
) | |
})} | |
</div> | |
</div> | |
))} | |
<button disabled={isVoting || hasVoted} type="submit"> | |
{isVoting | |
? "Votando..." | |
: hasVoted | |
? "Você já votou" | |
: "Submeter votos"} | |
</button> | |
{!hasVoted && ( | |
<small> | |
Isso irá submeter várias transações que você precisará assinar. | |
</small> | |
)} | |
</form> | |
</div> | |
</div> | |
</div> | |
) | |
}; | |
// Renderiza a tela de cunhagem do NFT. | |
return ( | |
<div className="mint-nft"> | |
<h1>Cunhe gratuitamente seu NFT de membro 🚴 da MTBDAO</h1> | |
<button | |
disabled={isClaiming} | |
onClick={mintNft} | |
> | |
{isClaiming ? "Cunhando..." : "Cunhe seu NFT (GRATIS)"} | |
</button> | |
</div> | |
) | |
} | |
export default App |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment