Como criei meu primeiro DApp do zero
Apostas esportivas
Muitos fãs ávidos de esportes gostam de fazer apostas em seus times favoritos na tentativa de obter lucros. No entanto, um problema é que muitos sites de apostas podem ser inseguros e exigir que terceiros manipulem e redistribuam seu dinheiro. Mas por que depositar nossa confiança nesses intermediários? É aqui que entram os DApps.
O que é um DApp?
Um aplicativo descentralizado, ou DApp, é um aplicativo que contém um contrato inteligente e é executado em uma rede distribuída, como a blockchain. Um contrato inteligente é um código executável na blockchain que é executado quando certas condições são atendidas.
Os contratos inteligentes eliminam a necessidade de intermediários e permitem que as transações sejam regidas e realizadas exclusivamente por códigos. Isso significa que todo o seu dinheiro é tratado por código, e não por terceiros potencialmente não confiáveis.
Existem muitos protocolos que podem ser usados para construir DApps, mas a rede principal e a que eu usei, é a Ethereum.
Artigo detalhado que escrevi sobre contratos inteligentes:
https://medium.com/visionary-hub/smart-contracts-are-the-future-962007fcb276
Estrutura do contrato
Antes de detalharmos o código do contrato, vamos listar as funções/variáveis que podemos precisar e dados que precisaremos armazenar.
Eventos
-
event NewBet
: armazena uma nova aposta na blockchain com endereço, valor apostado e equipe
Estruturas
struct Bet
: contém nome, endereço do usuário, valor que apostaram e equipe em que apostaramstruct Team
: contém nome da equipe e a quantidade total de ETH colocada em seu time
Variáveis/Mapeamentos/Matrizes
Bet[]
: matriz contendo todas as apostasTeam[]
: matriz contendo todos os timesaddress payable conOwner
: endereço do dono do contratouint totalBetMoney
: total de apostas feitas em ambos os timesmapping numBetsAddress
: vincula o endereço a uma aposta para garantir que cada usuário faça apenas uma aposta até que um vencedor seja escolhido
Funções
createTeam(_name)
: pode ser chamado pelo proprietário para criar um novo time na qual as apostas podem ser feitasgetTotalBetAmount(_teamId)
: pode ser chamado para obter o valor total da aposta feita em uma equipecreateBet(_name, _teamId)
: pode ser chamado por um usuário com msg.value para fazer uma aposta em uma determinada equipe (transferências de ETH para contrato)teamWinDistribution(_teamId)
: pode ser chamado pelo proprietário fazer uma equipe vencer e distribuir ETH para os vencedores de acordo.
Essencialmente, 2 equipes que estão predefinidas (Warriors/Nets neste caso) podem ser apostadas. Cada equipe tem um ID exclusivo (0/1) que um usuário pode usar para fazer uma aposta. Sempre que um usuário faz uma aposta, createBet
é chamado e sempre que o proprietário do contrato define uma equipe como vencedora, teamWinDistribution
é chamado.
Meu contrato também utilizava ownable.sol, ATM.sol e console.sol. O proprietário me permite adicionar modificadores de função, como onlyOwner
que permite apenas que o proprietário do contrato chame uma função. ATM é o que eu usei para facilitar a transferência de ETH entre contas. Console é uma biblioteca que ajudou na depuração e me permitiu visualizar variáveis durante a execução do contrato.
Passo a passo do código
pragma solidity 0.8.11;
import "./ownable.sol";
import "./ATM.sol";
import "./console.sol";
A linha um é para o compilador e especifica qual versão do Solidity pretendemos usar. As instruções de importação importam todos os contratos/bibliotecas externos que usaremos.
event NewBet(
address addy,
uint amount,
Team teamBet
);
No Solidity, um evento é chamado sempre que as informações precisam ser armazenadas na blockchain. Nesse caso, desejamos armazenar todas as novas apostas na blockchain com o endereço do usuário, a quantidade de ether que eles apostaram e o time em que fizeram uma aposta.
struct Bet {
string name;
address addy;
uint amount;
Team teamBet;
}
struct Team {
string name;
uint totalBetAmount;
}
As estruturas são semelhantes aos objetos, pois podem armazenar dados e serem usados para criar novas instâncias. Cada vez que uma aposta é feita, uma nova Bet
é instanciada e colocada em uma matriz de Apostas. A Team
é usada principalmente para armazenar o total de apostas feitas em uma equipe.
Bet[] public bets;
Team[] public teams;
address payable conOwner;
uint public totalBetMoney = 0;
mapping (address => uint) public numBetsAddress;
Uma matriz de Apostas/Equipes é mantida para armazenar todas as instâncias das estruturas. A conOwner
armazena o endereço do proprietário do contrato e é pagável para que o ETH possa ser transferido para a conta. totalBetMoney
começa em 0 e é usado para rastrear o total de apostas feitas em ambas as equipes. É atualizado sempre que uma nova aposta é criada.
Um mapeamento no Solidity armazena pares de chave e valor. Nesse caso, vinculamos um endereço a uma uint para acompanhar quantas apostas um usuário faz. Queremos garantir que cada usuário possa fazer apenas uma aposta por vez.
constructor() payable {
conOwner = payable(msg.sender); // setting the contract creator
teams.push(Team("team1", 0));
teams.push(Team("team2", 0));
}
O construtor é chamado sempre que o contrato é inicializado. Ele define conOwner
para o msg.sender
(ou a pessoa que chama a função). Ele também instancia duas equipes nas quais os usuários podem apostar.
function createTeam (string memory _name) public {
teams.push(Team(_name, 0));
}
function getTotalBetAmount (uint _teamId) public view returns (uint) {
return teams[_teamId].totalBetAmount;
}
A primeira função é createTeam
e é uma função pública que recebe um parâmetro chamado _name
. Essa função envia uma nova instância de Team para a matriz de equipes e é chamada dentro do construtor.
A segunda função é getTotalBetAmount
e é uma função de visualização pública, o que significa que recupera dados da blockchain. Esta função recebe _teamId
e retorna o total de apostas feitas naquela equipe.
function createBet (string memory _name, uint _teamId) external payable {
require (msg.sender != conOwner, "owner can't make a bet");
require (numBetsAddress[msg.sender] == 0, "you have already placed a bet");
require (msg.value > 0.01 ether, "bet more");
bets.push(Bet(_name, msg.sender, msg.value, teams[_teamId]));
if (_teamId == 0) {
teams[0].totalBetAmount += msg.value;
}
if (_teamId == 1) {
teams[1].totalBetAmount += msg.value;
}
numBetsAddress[msg.sender]++;
(bool sent, bytes memory data) = conOwner.call{value: msg.value}("");
require(sent, "Failed to send Ether");
totalBetMoney += msg.value;
emit NewBet(msg.sender, msg.value, teams[_teamId]);
}
createBet
é a primeira função principal do contrato e recebe _name
e _teamId
como parâmetros. A função é externa e pagável, o que significa que pode ser chamada por outros contratos e também facilitar uma transação na blockchain.
require
são usadas para garantir que certas pré-condições sejam verdadeiras antes que o código possa ser executado. Em primeiro lugar, o titular do contrato não pode fazer apostas. Segundo, um usuário só pode fazer uma aposta. Por fim, o valor da aposta Ether deve ser maior que 0,01.
Na linha 8, uma nova instância Bet é criada e enviada para a matriz de apostas. Se o _teamId
for 0, então o usuário está apostando no time 0 e seu totalBetAmount
deve ser aumentado por msg.value
. Se o _teamId
for 1, o totalBetAmount
será gerado por msg.value
. A função então atualiza o numBetsAddress
para documentar que o usuário acabou de criar uma aposta e não pode fazer outra.
As linhas 19–20 usam a call
para enviar ao contrato a quantidade de ETH que o usuário aposta.
Depois que o ETH é transferido, a totalBetMoney
aumenta em msg.value
e um novo evento de aposta é criado na blockchain.
function teamWinDistribution(uint _teamId) public payable onlyOwner() {
uint div;
if (_teamId == 0) {
for (uint i = 0; i < bets.length; i++) {
if (keccak256(abi.encodePacked((bets[i].teamBet.name))) == keccak256(abi.encodePacked("team1"))) {
address payable receiver = payable(bets[i].addy);
div = (bets[i].amount * (10000 + (getTotalBetAmount(1) * 10000 / getTotalBetAmount(0)))) / 10000;
(bool sent, bytes memory data) = receiver.call{ value: div }("");
require(sent, "Failed to send Ether");
}
}
} else {
for (uint i = 0; i < bets.length; i++) {
if (keccak256(abi.encodePacked((bets[i].teamBet.name))) == keccak256(abi.encodePacked("team2"))) {
address payable receiver = payable(bets[i].addy);
div = (bets[i].amount * (10000 + (getTotalBetAmount(0) * 10000 / getTotalBetAmount(1)))) / 10000;
(bool sent, bytes memory data) = receiver.call{ value: div }("");
require(sent, "Failed to send Ether");
}
}
}
totalBetMoney = 0;
teams[0].totalBetAmount = 0;
teams[1].totalBetAmount = 0;
for (uint i = 0; i < bets.length; i++) {
numBetsAddress[bets[i].addy] = 0;
}
}
teamWinDistribution
é a segunda função principal deste contrato e recebe uma entrada _teamId
. A função é pública e pagável, o que significa que pode ser chamada globalmente e também facilitar as transações. A função também possui o onlyOwner
que significa que apenas o proprietário do contrato pode definir um vencedor.
Uma variável chamada div é declarada e eventualmente conterá o valor que cada usuário ganha.
A função contém um grande if/else permitindo que ele aja de forma diferente com base em qual time vence. Cada vencedor recebe uma quantidade diferente de ETH, dependendo de quanto ETH eles entraram originalmente.
A equação acima é usada para determinar quanto ETH um vencedor recebe e é traduzida para o código na linha 9. Como o Solidity não suporta números de ponto fixo ou flutuante, temos que ajustar essa equação para acomodar. O valor final em wei é armazenado em div
.
As linhas 11/12 são usadas para enviar ao receiver
(endereço da aposta atual) a quantidade correta de ETH que eles ganharam. Após a conclusão de todas as transações, os totalBetMoney
e totalBetAmount
para ambas as equipes são redefinidos. Além disso, o numBetsAddress
fica vazio para que cada usuário possa apostar novamente.
Testando o Contrato
Para testar o contrato, usei principalmente o Remix e também usei trufas para checagem básica. Além disso, usei ganache para testar transações.
Configurando a Interação Front-End
Após criar o contrato, decidi desenvolver uma pequena UI usando React que roda em um servidor local. A parte mais difícil foi integrar as funções e variáveis do contrato inteligente no front-end. Embora não seja a interface do usuário mais limpa, é totalmente funcional e permite que os usuários façam apostas e apenas o proprietário faça uma equipe vencer.
Eu propositalmente não vou me aprofundar muito em como o front-end foi criado, pois o foco principal é o contrato e a funcionalidade de back-end.
Conclusão/Conclusões
Os contratos inteligentes são muito poderosos e têm muita autoridade, pois podem controlar transações e transformar a blockchain.
Os preços do gas são extremamente voláteis e podem realmente fazer ou quebrar seu DApp - no meu caso, o limite de gas da MetaMask era muito baixo, fazendo com que minhas transações falhassem às vezes.
Desenvolvimento Frontend é difícil e tedioso, mas necessário; construir a interface do usuário permite que qualquer pessoa utilize meu contrato com facilidade
Embora este DApp provavelmente não seja usado muito amplamente, eu me diverti muito construindo este projeto e aprendi muito mais sobre contratos/transações Ethereum.
Oi meu nome é Fazal. Eu sou um jovem de 16 anos da baía da Califórnia. Conecte-se comigo no Linkedin e confira meu Github para ver em quais outros projetos estou trabalhando! Assine a minha Newsletter para atualizações mensais sobre o que estou trabalhando. Se você quiser agendar uma reunião, use meu Calendly. Obrigado por ler!
Este artigo é do Fazal Mittu, foi traduzido por Arnaldo Campos e seu original pode ser lido aqui.
Top comments (0)