Contratos Oracle da Ethereum: Recursos do Solidity Code

Publicados: 2022-03-11

No primeiro segmento deste trio, passamos por um pequeno tutorial que nos deu um simples par de contrato com oráculo. Os mecanismos e processos de configuração (com trufa), compilação do código, implantação em uma rede de teste, execução e depuração foram descritos; no entanto, muitos dos detalhes do código foram encobertos de maneira ondulada. Então, agora, como prometido, veremos alguns desses recursos de linguagem que são exclusivos do desenvolvimento de contratos inteligentes do Solidity e exclusivos desse cenário de contrato-oráculo específico. Embora não possamos examinar minuciosamente todos os detalhes (deixarei isso para você em seus estudos posteriores, se desejar), tentaremos encontrar os recursos mais impressionantes, mais interessantes e mais importantes do código.

Para facilitar isso, recomendo que você abra sua própria versão do projeto (se tiver uma) ou tenha o código à mão para referência.

O código completo neste momento pode ser encontrado aqui: https://github.com/jrkosinski/oracle-example/tree/part2-step1

Ethereum e Solidez

Solidity não é a única linguagem de desenvolvimento de contrato inteligente disponível, mas acho que é seguro dizer que é a mais comum e mais popular em geral, para contratos inteligentes Ethereum. Certamente é o que tem o suporte e informações mais populares, no momento da redação deste artigo.

Diagrama de recursos cruciais do Ethereum Solidity

Solidity é orientado a objetos e Turing-completo. Dito isso, você perceberá rapidamente suas limitações internas (e totalmente intencionais), que fazem a programação de contrato inteligente parecer bem diferente do hacking comum do tipo vamos fazer isso.

Versão Solidity

Aqui está a primeira linha de cada poema de código Solidity:

 pragma solidity ^0.4.17;

Os números de versão que você vê serão diferentes, pois o Solidity, ainda em sua juventude, está mudando e evoluindo rapidamente. A versão 0.4.17 é a versão que usei em meus exemplos; a versão mais recente no momento desta publicação é 0.4.25.

A versão mais recente no momento em que você está lendo isso pode ser algo totalmente diferente. Muitos recursos interessantes estão em andamento (ou pelo menos planejados) para o Solidity, que discutiremos a seguir.

Aqui está uma visão geral das diferentes versões do Solidity.

Dica profissional: você também pode especificar uma variedade de versões (embora eu não veja isso com muita frequência), assim:

 pragma solidity >=0.4.16 <0.6.0;

Recursos da linguagem de programação Solidity

Solidity tem muitos recursos de linguagem que são familiares para a maioria dos programadores modernos, bem como alguns que são distintos e (pelo menos para mim) incomuns. Diz-se que foi inspirado em C++, Python e JavaScript - todos os quais são bem familiares para mim pessoalmente, e ainda assim o Solidity parece bastante distinto de qualquer uma dessas linguagens.

Contrato

O arquivo .sol é a unidade básica de código. Em BoxingOracle.sol, observe a 9ª linha:

 contract BoxingOracle is Ownable {

Como a classe é a unidade básica de lógica em linguagens orientadas a objetos, o contrato é a unidade básica de lógica no Solidity. Basta simplificá-lo por enquanto para dizer que o contrato é a “classe” do Solidity (para programadores orientados a objetos, esse é um salto fácil).

Herança

Os contratos do Solidity oferecem suporte total à herança e funcionam como esperado; membros de contratos privados não são herdados, enquanto os protegidos e públicos são. A sobrecarga e o polimorfismo são suportados como seria de esperar.

 contract BoxingOracle is Ownable {

Na declaração acima, a palavra-chave “is” está denotando herança. BoxingOracle herda de Ownable. A herança múltipla também é suportada no Solidity. A herança múltipla é indicada por uma lista delimitada por vírgulas de nomes de classes, assim:

 contract Child is ParentA, ParentB, ParentC { …

Embora (na minha opinião) não seja uma boa ideia ficar muito complicado ao estruturar seu modelo de herança, aqui está um artigo interessante sobre Solidity em relação ao chamado Diamond Problem.

Enums

Enums são suportados no Solidity:

 enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg, cancelled) Decided //index of participant who is the winner }

Como seria de esperar (não diferente de linguagens familiares), cada valor enum é atribuído a um valor inteiro, começando com 0. Conforme declarado nos documentos do Solidity, os valores enum são conversíveis para todos os tipos inteiros (por exemplo, uint, uint16, uint32, etc.), mas a conversão implícita não é permitida. O que significa que eles devem ser convertidos explicitamente (para uint, por exemplo).

Solidity Docs: Enums Tutorial do Enums

Estruturas

Structs são outra maneira, como enums, de criar um tipo de dados definido pelo usuário. Structs são familiares para todos os codificadores de base C/C++ e caras antigos como eu. Um exemplo de struct, da linha 17 do BoxingOracle.sol:

 //defines a match along with its outcome struct Match { bytes32 id; string name; string participants; uint8 participantCount; uint date; MatchOutcome outcome; int8 winner; }

Nota para todos os programadores antigos de C: Struct “packing” no Solidity é uma coisa, mas existem algumas regras e ressalvas. Não assuma necessariamente que funciona da mesma forma que em C; verifique a documentação e fique atento à sua situação, para saber se a mala vai ou não te ajudar em um determinado caso.

Embalagem de estrutura de solidez

Uma vez criados, os structs podem ser endereçados em seu código como tipos de dados nativos. Aqui está um exemplo da sintaxe para “instanciação” do tipo struct criado acima:

 Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);

Tipos de dados no Solidity

Isso nos leva ao assunto básico dos tipos de dados no Solidity. Quais tipos de dados o solidity suporta? Solidity é estaticamente tipado e, no momento da escrita, os tipos de dados devem ser declarados explicitamente e vinculados a variáveis.

Tipos de dados no Ethereum Solidity

Tipos de dados de solidez

Booleanos

Tipos booleanos são suportados sob o nome bool e valores true ou false

Tipos numéricos

Tipos inteiros são suportados, com e sem sinal, de int8/uint8 a int256/uint256 (isto é, inteiros de 8 bits a inteiros de 256 bits, respectivamente). O tipo uint é abreviação de uint256 (e da mesma forma int é abreviação de int256).

Notavelmente, os tipos de ponto flutuante não são suportados. Por que não? Bem, por um lado, ao lidar com valores monetários, as variáveis ​​de ponto flutuante são bem conhecidas por serem uma má ideia (em geral, é claro), porque o valor pode ser perdido no ar. Os valores de éter são indicados em wei, que é 1/1.000.000.000.000.000.000 de um éter, e isso deve ter precisão suficiente para todos os propósitos; você não pode quebrar um éter em partes menores.

Os valores de ponto fixo são parcialmente suportados neste momento. De acordo com os documentos do Solidity: “Os números de pontos fixos ainda não são totalmente suportados pelo Solidity. Eles podem ser declarados, mas não podem ser atribuídos a ou de”.

https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9

Observação: na maioria dos casos, é melhor usar apenas uint, pois diminuir o tamanho da variável (para uint32, por exemplo), pode realmente aumentar os custos de gás em vez de reduzi-los, como seria de esperar. Como regra geral, use uint a menos que tenha certeza de que tem uma boa razão para fazer o contrário.

Tipos de string

O tipo de dados string no Solidity é um assunto engraçado; você pode obter opiniões diferentes dependendo de quem você fala. Existe um tipo de dados string no Solidity, isso é um fato. Minha opinião, provavelmente compartilhada pela maioria, é que não oferece muita funcionalidade. Análise de string, concatenação, substituição, corte e até mesmo contagem do comprimento da string: nenhuma dessas coisas que você provavelmente espera de um tipo de string está presente e, portanto, são de sua responsabilidade (se você precisar delas). Algumas pessoas usam bytes32 no lugar de string; isso também pode ser feito.

Artigo divertido sobre cordas Solidity

Minha opinião: Pode ser um exercício divertido escrever seu próprio tipo de string e publicá-lo para uso geral.

tipo de endereço

Exclusivo talvez para Solidity, temos um tipo de dados de endereço , especificamente para carteira Ethereum ou endereços de contrato. É um valor de 20 bytes especificamente para armazenar endereços desse tamanho específico. Além disso, possui membros de tipo especificamente para endereços desse tipo.

 address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;

Tipos de dados de endereço

Tipos de data e hora

Não existe nenhum tipo nativo de Date ou DateTime no Solidity, por si só, como existe no JavaScript, por exemplo. (Ah, não — Solidity está soando cada vez pior a cada parágrafo!?) As datas são endereçadas nativamente como timestamps do tipo uint (uint256). Eles geralmente são tratados como timestamps no estilo Unix, em segundos em vez de milissegundos, pois o timestamp do bloco é um timestamp no estilo Unix. Nos casos em que você precisa de datas legíveis por vários motivos, existem bibliotecas de código aberto disponíveis. Você pode notar que eu usei um no BoxingOracle: DateLib.sol. O OpenZeppelin também possui utilitários de data, bem como muitos outros tipos de bibliotecas de utilitários gerais (em breve chegaremos ao recurso de biblioteca do Solidity).

Dica profissional: OpenZeppelin é uma boa fonte (mas é claro que não é a única boa fonte) tanto para conhecimento quanto para código genérico pré-escrito que pode ajudá-lo a construir seus contratos.

Mapeamentos

Observe que a linha 11 do BoxingOracle.sol define algo chamado mapeamento :

 mapping(bytes32 => uint) matchIdToIndex;

Um mapeamento no Solidity é um tipo de dados especial para pesquisas rápidas; essencialmente uma tabela de pesquisa ou semelhante a uma tabela de hash, em que os dados contidos residem no próprio blockchain (quando o mapeamento é definido, como aqui, como um membro da classe). Durante a execução do contrato, podemos adicionar dados ao mapeamento, semelhante a adicionar dados a uma tabela de hash, e depois pesquisar os valores que adicionamos. Observe novamente que, neste caso, os dados que adicionamos são adicionados ao próprio blockchain, portanto, persistirão. Se o adicionarmos ao mapeamento de hoje em Nova York, daqui a uma semana alguém em Istambul poderá lê-lo.

Exemplo de adição ao mapeamento, da linha 71 do BoxingOracle.sol:

 matchIdToIndex[id] = newIndex+1

Exemplo de leitura do mapeamento, da linha 51 do BoxingOracle.sol:

 uint index = matchIdToIndex[_matchId];

Os itens também podem ser removidos do mapeamento. Não é usado neste projeto, mas ficaria assim:

 delete matchIdToIndex[_matchId];

Valores de retorno

Como você deve ter notado, o Solidity pode ter uma semelhança superficial com o Javascript, mas não herda muito da frouxidão de tipos e definições do JavaScript. Um código de contrato deve ser definido de maneira bastante estrita e restrita (e isso provavelmente é uma coisa boa, considerando o caso de uso). Com isso em mente, considere a definição da função da linha 40 do BoxingOracle.sol

 function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }

OK, então, vamos primeiro fazer uma rápida visão geral do que está contido aqui. function marca-o como uma função. _getMatchIndex é o nome da função (o sublinhado é uma convenção que indica um membro privado — discutiremos isso mais tarde). Leva um argumento, chamado _matchId (desta vez, a convenção de sublinhado é usada para denotar argumentos de função) do tipo bytes32 . A palavra-chave private realmente torna o membro privado no escopo, view informa ao compilador que esta função não modifica nenhum dado no blockchain e, finalmente: ~~~ solidity retorna (uint) ~~~

Isso diz que a função retorna um uint (uma função que returns void simplesmente não teria nenhuma cláusula return aqui). Por que uint está entre parênteses? Isso porque as funções do Solidity podem e geralmente retornam tuplas .

Considere agora, a seguinte definição da linha 166:

 function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }

Confira a cláusula de retorno sobre este! Retorna uma, duas... sete coisas diferentes. OK, então, esta função retorna essas coisas como uma tupla. Por quê? No decorrer do desenvolvimento, muitas vezes você precisará retornar um struct (se fosse JavaScript, você gostaria de retornar um objeto JSON, provavelmente). Bem, no momento em que este artigo foi escrito (embora no futuro isso possa mudar), o Solidity não suporta o retorno de structs de funções públicas. Então você tem que retornar tuplas. Se você é um cara do Python, já deve estar confortável com tuplas. Muitos idiomas não os suportam, pelo menos não dessa maneira.

Veja a linha 159 para um exemplo de retorno de uma tupla como valor de retorno:

 return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);

E como aceitamos o valor de retorno de algo assim? Podemos fazer assim:

 var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);

Alternativamente, você pode declarar as variáveis ​​explicitamente de antemão, com seus tipos corretos:

 //declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);

E agora declaramos 7 variáveis ​​para armazenar os 7 valores de retorno, que agora podemos usar. Caso contrário, supondo que queríamos apenas um ou dois dos valores, podemos dizer:

 //declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);

Veja o que fizemos lá? Temos apenas os dois nos quais estávamos interessados. Confira todas essas vírgulas. Temos que contá-los com cuidado!

Importações

As linhas 3 e 4 do BoxingOracle.sol são importações:

 import "./Ownable.sol"; import "./DateLib.sol";

Como você provavelmente espera, eles estão importando definições de arquivos de código que existem na mesma pasta de projeto de contratos que BoxingOracle.sol.

Modificadores

Observe que as definições de função têm vários modificadores anexados. Primeiro, há visibilidade: visibilidade de função privada, pública, interna e externa.

Além disso, você verá palavras-chave pure e view . Eles indicam ao compilador que tipo de alterações a função fará, se houver. Isso é importante porque tal coisa é um fator no custo final do gás para executar a função. Veja aqui a explicação: Solidity Docs.

Finalmente, o que eu realmente quero discutir são os modificadores personalizados. Dê uma olhada na linha 61 do BoxingOracle.sol:

 function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {

Observe o modificador onlyOwner logo antes da palavra-chave “public”. Isso indica que apenas o proprietário do contrato pode chamar esse método! Embora muito importante, este não é um recurso nativo do Solidity (embora talvez seja no futuro). Na verdade, onlyOwner é um exemplo de modificador personalizado que nós mesmos criamos e usamos. Vamos dar uma olhada.

Primeiro, o modificador é definido no arquivo Ownable.sol, que você pode ver que importamos na linha 3 do BoxingOracle.sol:

 import "./Ownable.sol"

Observe que, para usar o modificador, fizemos BoxingOracle herdar de Ownable . Dentro do Ownable.sol, na linha 25, podemos encontrar a definição do modificador dentro do contrato “Ownable”:

 modifier onlyOwner() { require(msg.sender == owner); _; }

(Este contrato de propriedade, a propósito, é retirado de um dos contratos públicos do OpenZeppelin.)

Observe que essa coisa é declarada como um modificador, indicando que podemos usá-la como temos, para modificar uma função. Observe que a carne do modificador é uma instrução “requer”. Requer instruções são como asserções, mas não para depuração. Se a condição da instrução require falhar, a função lançará uma exceção. Então, parafraseando esta declaração “requer”:

 require(msg.sender == owner);

Poderíamos dizer que significa:

 if (msg.send != owner) throw an exception;

E, de fato, no Solidity 0.4.22 e superior, podemos adicionar uma mensagem de erro a essa instrução require:

 require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");

Finalmente, na linha curiosa:

 _;

O sublinhado é uma abreviação de "Aqui, execute o conteúdo completo da função modificada". Então, na verdade, a instrução require será executada primeiro, seguida pela função real. Então é como pré-pender essa linha de lógica para a função modificada.

Há, é claro, mais coisas que você pode fazer com modificadores. Verifique os documentos: Documentos.

Bibliotecas Solidity

Existe um recurso de linguagem do Solidity conhecido como biblioteca . Temos um exemplo em nosso projeto em DateLib.sol.

Implementação da Biblioteca Solidity!

Esta é uma biblioteca para melhor manuseio mais fácil de tipos de data. É importado para BoxingOracle na linha 4:

 import "./DateLib.sol";

E é usado na linha 13:

 using DateLib for DateLib.DateTime;

DateLib.DateTime é um struct que é exportado do contrato DateLib (é exposto como membro; veja a linha 4 de DateLib.sol) e estamos declarando aqui que estamos “usando” a biblioteca DateLib para um determinado tipo de dados. Portanto, os métodos e operações declarados nessa biblioteca serão aplicados ao tipo de dados que dissemos que deveria. É assim que uma biblioteca é usada no Solidity.

Para um exemplo mais claro, confira algumas das bibliotecas do OpenZeppelin para números, como SafeMath. Eles podem ser aplicados a tipos de dados Solidity nativos (numéricos) (enquanto aqui aplicamos uma biblioteca a um tipo de dados personalizado) e são amplamente utilizados.

Interfaces

Como nas linguagens orientadas a objetos convencionais, as interfaces são suportadas. Interfaces no Solidity são definidas como contratos, mas os corpos de função são omitidos para as funções. Para obter um exemplo de definição de interface, consulte OracleInterface.sol. Neste exemplo, a interface é usada como um substituto para o contrato oracle, cujo conteúdo reside em um contrato separado com um endereço separado.

Convenções de nomenclatura

É claro que as convenções de nomenclatura não são uma regra global; como programadores, sabemos que somos livres para seguir as convenções de codificação e nomenclatura que nos atraem. Por outro lado, queremos que os outros se sintam à vontade para ler e trabalhar com nosso código, portanto, algum grau de padronização é desejável.

Visão Geral do Projeto

Portanto, agora que examinamos alguns recursos gerais de linguagem presentes nos arquivos de código em questão, podemos começar a dar uma olhada mais específica no próprio código, para este projeto.

Então, vamos esclarecer o propósito deste projeto, mais uma vez. O objetivo deste projeto é fornecer uma demonstração semi-realista (ou pseudo-realista) e exemplo de um contrato inteligente que usa um oráculo. No fundo, este é apenas um contrato que exige outro contrato separado.

O caso de negócios do exemplo pode ser enunciado da seguinte forma:

  • Um usuário deseja fazer apostas de tamanhos variados em lutas de boxe, pagando dinheiro (éter) pelas apostas e coletando seus ganhos quando e se vencerem.
  • Um usuário faz essas apostas por meio de um contrato inteligente. (Em um caso de uso real, isso seria um DApp completo com um front-end web3; mas estamos apenas examinando o lado dos contratos.)
  • Um contrato inteligente separado – o oráculo – é mantido por terceiros. Seu trabalho é manter uma lista de lutas de boxe com seus estados atuais (pendente, em andamento, finalizado etc.) e, se finalizado, o vencedor.
  • O contrato principal obtém listas de partidas pendentes do oráculo e as apresenta aos usuários como partidas “apostas”.
  • O contrato principal aceita apostas até o início de uma partida.
  • Depois que uma partida é decidida, o contrato principal divide os ganhos e perdas de acordo com um algoritmo simples, recebe uma parte e paga os ganhos mediante solicitação (os perdedores simplesmente perdem toda a sua aposta).

As regras de apostas:

  • Existe uma aposta mínima definida (definida em wei).
  • Não há aposta máxima; os usuários podem apostar qualquer quantia acima do mínimo.
  • Os usuários podem fazer apostas até o momento em que a partida se torna “em andamento”.

Algoritmo para dividir os ganhos:

  • Todas as apostas recebidas são colocadas em um “pote”.
  • Uma pequena porcentagem é retirada do pote, para a casa.
  • Cada vencedor recebe uma proporção do pote, diretamente proporcional ao tamanho relativo de suas apostas.
  • Os ganhos são calculados assim que o primeiro usuário solicita os resultados, após a decisão da partida.
  • Os ganhos são concedidos mediante solicitação do usuário.
  • Em caso de empate, ninguém ganha – todos recebem sua aposta de volta, e a casa não recebe nenhum corte.

BoxingOracle: o contrato Oracle

Principais funções fornecidas

O oráculo possui duas interfaces, pode-se dizer: uma apresentada ao “dono” e mantenedor do contrato e outra apresentada ao público em geral; isto é, contratos que consomem o oráculo. O mantenedor, oferece funcionalidade para alimentar dados no contrato, essencialmente pegando dados do mundo exterior e colocando-os no blockchain. Ao público, oferece acesso somente leitura a esses dados. É importante observar que o próprio contrato restringe os não proprietários de editar quaisquer dados, mas o acesso somente leitura a esses dados é concedido publicamente sem restrições.

Para usuários:

  • Listar todas as partidas
  • Listar correspondências pendentes
  • Obter detalhes de uma partida específica
  • Obtenha o status e o resultado de uma partida específica

Ao proprietário:

  • Digite uma correspondência
  • Alterar o status da partida
  • Definir resultado da partida

Ilustração de elementos de acesso de usuário e proprietário

História do usuário:

  • Uma nova luta de boxe é anunciada e confirmada para o dia 9 de maio.
  • Eu, o mantenedor do contrato (talvez eu seja uma conhecida rede esportiva ou um novo canal), adiciono a próxima partida aos dados do oráculo na blockchain, com o status “pendente”. Qualquer pessoa ou qualquer contrato agora pode consultar e usar esses dados como quiser.
  • Quando a partida começa, defino o status dessa partida como "em andamento".
  • Quando a partida termina, defino o status da partida como “concluída” e modifico os dados da partida para indicar o vencedor.

Revisão do Código Oracle

Esta revisão é baseada inteiramente em BoxingOracle.sol; os números de linha fazem referência a esse arquivo.

Nas linhas 10 e 11, declaramos nosso local de armazenamento para partidas:

 Match[] matches; mapping(bytes32 => uint) matchIdToIndex;

matches é apenas uma matriz simples para armazenar instâncias de correspondência, e o mapeamento é apenas um recurso para mapear um ID de correspondência exclusivo (um valor bytes32) para seu índice na matriz para que, se alguém nos der um ID bruto de uma correspondência, possamos use este mapeamento para localizá-lo.

Na linha 17, nossa estrutura de partidas é definida e explicada:

 //defines a match along with its outcome struct Match { bytes32 id; //unique id string name; //human-friendly name (eg, Jones vs. Holloway) string participants; //a delimited string of participant names uint8 participantCount; //number of participants (always 2 for boxing matches!) uint date; //GMT timestamp of date of contest MatchOutcome outcome; //the outcome (if decided) int8 winner; //index of the participant who is the winner } //possible match outcomes enum MatchOutcome { Pending, //match has not been fought to decision Underway, //match has started & is underway Draw, //anything other than a clear winner (eg, cancelled) Decided //index of participant who is the winner }

Linha 61: A função addMatch é para uso apenas do proprietário do contrato; permite a adição de uma nova correspondência aos dados armazenados.

Linha 80: A função declareOutcome permite que o proprietário do contrato defina uma partida como “decidida”, definindo o participante que ganhou.

Linhas 102-166: As funções a seguir podem ser chamadas pelo público. Estes são os dados somente leitura abertos ao público em geral:

  • A função getPendingMatches retorna uma lista de IDs de todas as correspondências cujo estado atual é "pendente".
  • A função getAllMatches retorna uma lista de IDs de todas as correspondências.
  • A função getMatch retorna os detalhes completos de uma única correspondência, especificada por ID.

As linhas 193-204 declaram funções que são principalmente para teste, depuração e diagnóstico.

  • A função testConnection apenas testa se podemos chamar o contrato.
  • A função getAddress retorna o endereço deste contrato.
  • A função addTestData adiciona várias correspondências de teste à lista de correspondências.

Sinta-se à vontade para explorar um pouco o código antes de passar para as próximas etapas. Sugiro executar o contrato oracle novamente no modo de depuração (conforme descrito na Parte 1 desta série), chamar funções diferentes e examinar os resultados.

BoxingBets: O contrato do cliente

É importante definir pelo que o contrato do cliente (o contrato de apostas) é responsável e pelo que não é responsável. O contrato do cliente não é responsável por manter listas de lutas reais de boxe ou declarar seus resultados. Nós “confiamos” (sim, eu sei, existe aquela palavra delicada – uh oh – discutiremos isso na Parte 3) no oráculo para aquele culto. O contrato do cliente é responsável por aceitar as apostas. É responsável pelo algoritmo que divide os ganhos e os transfere para as contas dos vencedores com base no resultado da partida (conforme recebido do oráculo).

Além disso, tudo é baseado em pull e não há eventos ou pushes. O contrato extrai dados do oráculo. O contrato extrai o resultado da partida do oráculo (em resposta à solicitação do usuário) e o contrato calcula os ganhos e os transfere em resposta à solicitação do usuário.

Principais funções fornecidas

  • Listar todas as correspondências pendentes
  • Obter detalhes de uma partida específica
  • Obtenha o status e o resultado de uma partida específica
  • Fazer uma aposta
  • Solicitar/receber ganhos

Revisão do código do cliente

Esta revisão é baseada inteiramente em BoxingBets.sol; os números de linha fazem referência a esse arquivo.

As linhas 12 e 13, as primeiras linhas de código do contrato, definem alguns mapeamentos nos quais armazenaremos os dados do nosso contrato.

A linha 12 mapeia endereços de usuários para listas de IDs. Isso está mapeando um usuário para uma lista de IDs de apostas que pertencem ao usuário. Assim, para qualquer endereço de usuário, podemos obter rapidamente uma lista de todas as apostas feitas por esse usuário.

 mapping(address => bytes32[]) private userToBets;

A linha 13 mapeia o ID exclusivo de uma partida para uma lista de instâncias de aposta. Com isso, podemos, para qualquer partida, obter uma lista de todas as apostas que foram feitas para aquela partida.

 mapping(bytes32 => Bet[]) private matchToBets;

As linhas 17 e 18 estão relacionadas à conexão com nosso oráculo. Primeiro, na variável boxingOracleAddr , armazenamos o endereço do contrato oracle (definido como zero por padrão). Poderíamos codificar o endereço do oráculo, mas nunca seríamos capazes de alterá-lo. (Não ser capaz de mudar o endereço do oráculo pode ser uma coisa boa ou ruim – podemos discutir isso na Parte 3). A próxima linha cria uma instância da interface do oráculo (que é definida em OracleInterface.sol) e a armazena em uma variável.

 //boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);

Se você pular para a linha 58, verá a função setOracleAddress , na qual esse endereço oracle pode ser alterado e na qual a instância boxingOracle é reinstanciada com um novo endereço.

A linha 21 define nosso tamanho mínimo de aposta, em wei. Esta é, na verdade, uma quantidade muito pequena, apenas 0,000001 éter.

 uint internal minimumBet = 1000000000000;

Nas linhas 58 e 66 respectivamente, temos as setOracleAddress e getOracleAddress . O setOracleAddress tem o modificador onlyOwner porque somente o dono do contrato pode trocar o oráculo por outro oráculo (provavelmente não é uma boa ideia, mas vamos elaborar na Parte 3). A função getOracleAddress , por outro lado, pode ser chamada publicamente; qualquer um pode ver qual oráculo está sendo usado.

 function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....

Nas linhas 72 e 79, temos as funções getBettableMatches e getMatch , respectivamente. Observe que eles estão simplesmente encaminhando as chamadas para o oráculo e retornando o resultado.

 function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....

A função placeBet é muito importante (linha 108).

 function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...

Uma característica marcante deste é o modificador payable ; estivemos tão ocupados discutindo os recursos gerais da linguagem que ainda não tocamos no recurso centralmente importante de poder enviar dinheiro junto com chamadas de função! Isso é basicamente o que é - é uma função que pode aceitar uma quantia de dinheiro junto com quaisquer outros argumentos e dados enviados.

Precisamos disso aqui porque é aqui que o usuário define simultaneamente qual aposta vai fazer, quanto dinheiro pretende ter nessa aposta e, de fato, envia o dinheiro. O modificador payable permite isso. Antes de aceitar a aposta, fazemos várias verificações para garantir a validade da aposta. A primeira verificação na linha 111 é:

 require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");

A quantidade de dinheiro enviada é armazenada em msg.value . Supondo que todos os cheques passem, na linha 123, transferiremos esse valor para a propriedade do oráculo, tirando a propriedade desse valor do usuário e para a posse do contrato:

 address(this).transfer(msg.value);

Por fim, na linha 136, temos uma função auxiliar de teste/depuração que nos ajudará a saber se o contrato está ou não conectado a um oráculo válido:

 function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }

Empacotando

E isso é, na verdade, até onde este exemplo vai; apenas aceitando a aposta. A funcionalidade de dividir os ganhos e pagar, bem como alguma outra lógica foi intencionalmente deixada de lado para manter o exemplo simples o suficiente para nosso propósito, que é simplesmente demonstrar o uso de um oráculo com um contrato. Essa lógica mais completa e complexa existe em outro projeto atualmente, que é uma extensão deste exemplo e ainda está em desenvolvimento.

Portanto, agora temos uma melhor compreensão da base de código e a usamos como veículo e ponto de partida para discutir alguns dos recursos de linguagem oferecidos pelo Solidity. O objetivo principal desta série de três partes é demonstrar e discutir o uso de um contrato com um oráculo. O objetivo desta parte é entender um pouco melhor esse código específico e usá-lo como um ponto de partida para entender alguns recursos do Solidity e do desenvolvimento de contratos inteligentes. O objetivo da terceira e última parte será discutir a estratégia e filosofia de uso do oráculo e como ele se encaixa conceitualmente no modelo de contrato inteligente.

Outras etapas opcionais

Eu encorajaria os leitores que desejam aprender mais, a pegar este código e brincar com ele. Implemente novos recursos. Corrija quaisquer erros. Implemente recursos não implementados (como a interface de pagamento). Teste as chamadas de função. Modifique-os e teste novamente para ver o que acontece. Adicione um front-end web3. Adicione um recurso para remover partidas ou modificar seus resultados (em caso de erro). E as partidas canceladas? Implemente um segundo oráculo. Claro, um contrato é livre para usar quantos oráculos quiser, mas que problemas isso acarreta? Divirta-se com isso; essa é uma ótima maneira de aprender, e quando você faz dessa maneira (e se diverte com isso), com certeza retém mais do que aprendeu.

Uma lista de amostra não abrangente de coisas para tentar:

  • Execute o contrato e o oráculo na rede de teste local (em trufa, conforme descrito na Parte 1) e chame todas as funções que podem ser chamadas e todas as funções de teste.
  • Adicione funcionalidade para calcular os ganhos e pagá-los, após a conclusão de uma partida.
  • Adicionada funcionalidade para reembolsar todas as apostas em caso de empate.
  • Adicione um recurso para solicitar um reembolso ou cancelar uma aposta antes do início da partida.
  • Adicione um recurso para permitir que as partidas às vezes sejam canceladas (nesse caso, todos precisarão de um reembolso).
  • Implemente um recurso para garantir que o oráculo que estava em vigor quando um usuário fez uma aposta é o mesmo oráculo que será usado para determinar o resultado dessa partida.
  • Implemente outro (segundo) oráculo, que tenha alguns recursos diferentes associados a ele, ou possivelmente sirva a um esporte diferente do boxe (observe que a contagem e a lista de participantes permitem diferentes tipos de esportes, portanto, não estamos restritos apenas ao boxe) .
  • Implemente getMostRecentMatch para que ele realmente retorne a correspondência adicionada mais recentemente ou a correspondência mais próxima da data atual em termos de quando ela ocorrerá.
  • Implemente o tratamento de exceções.

Quando você estiver familiarizado com a mecânica do relacionamento entre o contrato e o oráculo, na Parte 3 desta série de três partes, discutiremos algumas das questões estratégicas, de design e filosóficas levantadas por este exemplo.