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.