WEB3DEV

Cover image for Construindo um DApp de Apostas Esportivas
Arnaldo Pereira Campos Junior
Arnaldo Pereira Campos Junior

Posted on • Atualizado em

Construindo um DApp de Apostas Esportivas

Como criei meu primeiro DApp do zero

Image description

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?

Image description

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 apostaram

  • struct 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 apostas

  • Team[]: matriz contendo todos os times

  • address payable conOwner: endereço do dono do contrato

  • uint totalBetMoney: total de apostas feitas em ambos os times

  • mapping 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 feitas

  • getTotalBetAmount(_teamId): pode ser chamado para obter o valor total da aposta feita em uma equipe

  • createBet(_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";
Enter fullscreen mode Exit fullscreen mode

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
);
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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));
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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]);

}
Enter fullscreen mode Exit fullscreen mode

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;
    }

}
Enter fullscreen mode Exit fullscreen mode

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.

Image description

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.

Image description

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)