Contratos de Ethereum Oracle: características del código de solidez

Publicado: 2022-03-11

En el primer segmento de este artículo de tres partes, pasamos por un pequeño tutorial que nos dio un par simple de contrato con oráculo. Se describieron los mecanismos y procesos de configuración (con trufa), compilación del código, implementación en una red de prueba, ejecución y depuración; sin embargo, muchos de los detalles del código se pasaron por alto de forma ondulada a mano. Así que ahora, como se prometió, veremos algunas de esas características del lenguaje que son exclusivas del desarrollo de contratos inteligentes de Solidity y exclusivas de este escenario de oráculo de contratos en particular. Si bien no podemos analizar minuciosamente cada detalle (se lo dejaré a usted en sus estudios posteriores, si lo desea), intentaremos dar con las características más llamativas, más interesantes y más importantes del código.

Para facilitar esto, le recomiendo que abra su propia versión del proyecto (si tiene una) o que tenga el código a mano como referencia.

El código completo en este punto se puede encontrar aquí: https://github.com/jrkosinski/oracle-example/tree/part2-step1

Ethereum y solidez

Solidity no es el único lenguaje de desarrollo de contratos inteligentes disponible, pero creo que es lo suficientemente seguro como para decir que es el más común y popular en general para los contratos inteligentes de Ethereum. Sin duda, es el que tiene el apoyo y la información más popular, en el momento de escribir este artículo.

Diagrama de características cruciales de Ethereum Solidity

La solidez está orientada a objetos y completa de Turing. Dicho esto, rápidamente se dará cuenta de sus limitaciones integradas (y totalmente intencionales), que hacen que la programación de contratos inteligentes se sienta bastante diferente de la piratería ordinaria de vamos a hacer esto.

Versión Solidez

Aquí está la primera línea de cada poema en código de Solidity:

 pragma solidity ^0.4.17;

Los números de versión que ve van a diferir, ya que Solidity, aún en su juventud, está cambiando y evolucionando rápidamente. La versión 0.4.17 es la versión que he usado en mis ejemplos; la última versión en el momento de esta publicación es 0.4.25.

La última versión en este momento que está leyendo esto bien puede ser algo completamente diferente. Muchas características agradables están en proceso (o al menos planeadas) para Solidity, que discutiremos en este momento.

Aquí hay una descripción general de las diferentes versiones de Solidity.

Consejo profesional: también puede especificar un rango de versiones (aunque no veo que esto se haga con demasiada frecuencia), así:

 pragma solidity >=0.4.16 <0.6.0;

Características del lenguaje de programación Solidity

Solidity tiene muchas características de lenguaje que son familiares para la mayoría de los programadores modernos, así como algunas que son distintas y (al menos para mí) inusuales. Se dice que se inspiró en C ++, Python y JavaScript, todos los cuales me son muy familiares personalmente y, sin embargo, Solidity parece bastante distinto de cualquiera de esos lenguajes.

Contrato

El archivo .sol es la unidad básica de código. En BoxingOracle.sol, observe la novena línea:

 contract BoxingOracle is Ownable {

Como la clase es la unidad básica de lógica en los lenguajes orientados a objetos, el contrato es la unidad básica de lógica en Solidity. Baste para simplificarlo por ahora decir que el contrato es la "clase" de Solidity (para los programadores orientados a objetos, este es un salto fácil).

Herencia

Los contratos de solidez son totalmente compatibles con la herencia y funcionan como cabría esperar; los miembros del contrato privado no se heredan, mientras que los protegidos y los públicos sí. La sobrecarga y el polimorfismo son compatibles como era de esperar.

 contract BoxingOracle is Ownable {

En la declaración anterior, la palabra clave "es" denota herencia. BoxingOracle hereda de Ownable. La herencia múltiple también es compatible con Solidity. La herencia múltiple se indica mediante una lista delimitada por comas de nombres de clase, así:

 contract Child is ParentA, ParentB, ParentC { …

Si bien (en mi opinión) no es una buena idea complicarse demasiado al estructurar su modelo de herencia, aquí hay un artículo interesante sobre la solidez con respecto al llamado problema del diamante.

Enumeraciones

Las enumeraciones son compatibles con 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 era de esperar (no diferente de los lenguajes familiares), a cada valor de enumeración se le asigna un valor entero, comenzando con 0. Como se indica en los documentos de Solidity, los valores de enumeración se pueden convertir a todos los tipos de enteros (p. ej., uint, uint16, uint32, etc.), pero no se permite la conversión implícita. Lo que significa que deben emitirse explícitamente (a uint, por ejemplo).

Solidity Docs: Enumeraciones Tutorial de enumeraciones

estructuras

Las estructuras son otra forma, como las enumeraciones, de crear un tipo de datos definido por el usuario. Las estructuras son familiares para todos los codificadores básicos de C/C++ y para los viejos como yo. Un ejemplo de una estructura, de la línea 17 de 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 los antiguos programadores de C: el "empaquetado" de estructuras en Solidity es una cosa, pero hay algunas reglas y advertencias. No asuma necesariamente que funciona igual que en C; consulte los documentos y esté al tanto de su situación, para determinar si el embalaje lo ayudará o no en un caso determinado.

Embalaje de estructura de solidez

Una vez creadas, las estructuras se pueden abordar en su código como tipos de datos nativos. Aquí hay un ejemplo de la sintaxis para la "instanciación" del tipo de estructura creado anteriormente:

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

Tipos de datos en Solidity

Esto nos lleva al tema muy básico de los tipos de datos en Solidity. ¿Qué tipos de datos admite Solidity? La solidez tiene un tipo estático y, en el momento de escribir esto, los tipos de datos deben declararse explícitamente y vincularse a las variables.

Tipos de datos en Ethereum Solidity

Tipos de datos de solidez

Booleanos

Los tipos booleanos se admiten con el nombre bool y los valores son verdadero o falso

Tipos numéricos

Se admiten tipos enteros, tanto con signo como sin signo, desde int8/uint8 hasta int256/uint256 (es decir, enteros de 8 bits a enteros de 256 bits, respectivamente). El tipo uint es la abreviatura de uint256 (y del mismo modo int es la abreviatura de int256).

En particular, los tipos de punto flotante no son compatibles. ¿Por qué no? Bueno, por un lado, cuando se trata de valores monetarios, se sabe que las variables de punto flotante son una mala idea (en general, por supuesto), porque el valor puede perderse en el aire. Los valores de éter se denotan en wei, que es 1/1.000.000.000.000.000.000 de un éter, y debe ser suficiente precisión para todos los propósitos; no se puede descomponer un éter en partes más pequeñas.

Los valores de puntos fijos se admiten parcialmente en este momento. De acuerdo con los documentos de Solidity: “Los números de puntos fijos aún no son totalmente compatibles con Solidity. Se pueden declarar, pero no se pueden asignar a o desde”.

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

Nota: En la mayoría de los casos, es mejor usar uint, ya que disminuir el tamaño de la variable (a uint32, por ejemplo) puede aumentar los costos de gasolina en lugar de disminuirlos como cabría esperar. Como regla general, use uint a menos que esté seguro de que tiene una buena razón para hacerlo de otra manera.

Tipos de cadenas

El tipo de datos de cadena en Solidity es un tema divertido; puede obtener diferentes opiniones dependiendo de con quién hable. Hay un tipo de datos de cadena en Solidity, eso es un hecho. Mi opinión, probablemente compartida por la mayoría, es que no ofrece mucha funcionalidad. Análisis de cadenas, concatenación, reemplazo, recorte, incluso contar la longitud de la cadena: ninguna de las cosas que probablemente espera de un tipo de cadena están presentes, por lo que son su responsabilidad (si las necesita). Algunas personas usan bytes32 en lugar de cadena; eso también se puede hacer.

Divertido artículo sobre las cuerdas Solidity

Mi opinión: podría ser un ejercicio divertido escribir su propio tipo de cadena y publicarlo para uso general.

Tipo de dirección

Exclusivo tal vez de Solidity, tenemos un tipo de datos de dirección , específicamente para billetera Ethereum o direcciones de contrato. Es un valor de 20 bytes específicamente para almacenar direcciones de ese tamaño en particular. Además, tiene miembros tipo específicamente para direcciones de ese tipo.

 address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;

Tipos de datos de direcciones

Tipos de fecha y hora

No hay un tipo nativo de fecha o fecha y hora en Solidity, per se, como lo hay en JavaScript, por ejemplo. (Oh, no, ¿¡Solidity suena cada vez peor con cada párrafo!?) Las fechas se abordan de forma nativa como marcas de tiempo de tipo uint (uint256). Por lo general, se manejan como marcas de tiempo de estilo Unix, en segundos en lugar de milisegundos, ya que la marca de tiempo del bloque es una marca de tiempo de estilo Unix. En los casos en los que necesite fechas legibles por humanos por varias razones, hay bibliotecas de código abierto disponibles. Puede notar que he usado uno en BoxingOracle: DateLib.sol. OpenZeppelin también tiene utilidades de fecha, así como muchos otros tipos de bibliotecas de utilidades generales (en breve llegaremos a la función de biblioteca de Solidity).

Consejo profesional: OpenZeppelin es una buena fuente (pero, por supuesto, no la única buena fuente) tanto para el conocimiento como para el código genérico preescrito que puede ayudarlo a desarrollar sus contratos.

Asignaciones

Observe que la línea 11 de BoxingOracle.sol define algo llamado mapeo :

 mapping(bytes32 => uint) matchIdToIndex;

Un mapeo en Solidity es un tipo de datos especial para búsquedas rápidas; esencialmente una tabla de búsqueda o similar a una tabla hash, en la que los datos contenidos viven en la propia cadena de bloques (cuando el mapeo se define, como está aquí, como un miembro de la clase). Durante el curso de la ejecución del contrato, podemos agregar datos al mapeo, similar a agregar datos a una tabla hash, y luego buscar los valores que hemos agregado. Tenga en cuenta nuevamente que, en este caso, los datos que agregamos se agregan a la propia cadena de bloques, por lo que persistirán. Si lo añadimos al mapeo hoy en Nueva York, dentro de una semana alguien en Estambul podrá leerlo.

Ejemplo de adición al mapeo, de la línea 71 de BoxingOracle.sol:

 matchIdToIndex[id] = newIndex+1

Ejemplo de lectura del mapeo, de la línea 51 de BoxingOracle.sol:

 uint index = matchIdToIndex[_matchId];

Los elementos también se pueden eliminar del mapeo. No se usa en este proyecto, pero se vería así:

 delete matchIdToIndex[_matchId];

Valores devueltos

Como habrás notado, Solidity puede tener un parecido superficial con Javascript, pero no hereda mucho de la soltura de tipos y definiciones de JavaScript. Un código de contrato debe definirse de una manera bastante estricta y restringida (y esto probablemente sea algo bueno, considerando el caso de uso). Con eso en mente, considere la definición de función de la línea 40 de BoxingOracle.sol

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

Bien, entonces, primero hagamos una descripción general rápida de lo que está contenido aquí. function lo marca como una función. _getMatchIndex es el nombre de la función (el guión bajo es una convención que indica un miembro privado; lo discutiremos más adelante). Toma un argumento, llamado _matchId (esta vez se usa la convención de guión bajo para denotar argumentos de función) de tipo bytes32 . La palabra clave private en realidad hace que el miembro tenga un alcance privado, view le dice al compilador que esta función no modifica ningún dato en la cadena de bloques y, finalmente: ~~~ solidity return (uint) ~~~

Esto dice que la función devuelve un uint (una función que devuelve void simplemente no tendría una cláusula de returns aquí). ¿Por qué está uint entre paréntesis? Esto se debe a que las funciones de Solidity pueden y, a menudo, devuelven tuplas .

Considere ahora, la siguiente definición de la línea 166:

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

¡Mira la cláusula de devolución de este! Devuelve una, dos… siete cosas distintas. Bien, esta función devuelve estas cosas como una tupla. ¿Por qué? En el curso del desarrollo, a menudo se encontrará con la necesidad de devolver una estructura (si fuera JavaScript, probablemente querría devolver un objeto JSON). Bueno, a partir de este escrito (aunque en el futuro esto puede cambiar), Solidity no admite la devolución de estructuras desde funciones públicas. Así que tienes que devolver tuplas en su lugar. Si eres un chico de Python, es posible que ya te sientas cómodo con las tuplas. Sin embargo, muchos idiomas realmente no los admiten, al menos no de esta manera.

Consulte la línea 159 para ver un ejemplo de devolución de una tupla como valor de devolución:

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

¿Y cómo aceptamos el valor de retorno de algo como esto? Podemos hacer así:

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

Alternativamente, puede declarar las variables explícitamente de antemano, con sus tipos correctos:

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

Y ahora hemos declarado 7 variables para contener los 7 valores devueltos, que ahora podemos usar. De lo contrario, suponiendo que quisiéramos solo uno o dos de los valores, podemos decir:

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

Ves lo que hicimos allí? Solo tenemos los dos que nos interesaban. Echa un vistazo a todas esas comas. ¡Tenemos que contarlos con cuidado!

Importaciones

Las líneas 3 y 4 de BoxingOracle.sol son importaciones:

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

Como probablemente espera, se trata de definiciones de importación de archivos de código que existen en la misma carpeta de proyectos de contratos que BoxingOracle.sol.

Modificadores

Tenga en cuenta que las definiciones de función tienen un montón de modificadores adjuntos. En primer lugar, está la visibilidad: privada, pública, interna y externa: visibilidad de funciones.

Además, verá las palabras clave pure y view . Estos indican al compilador qué tipo de cambios hará la función, si los hay. Esto es importante porque tal cosa es un factor en el costo final del gas para ejecutar la función. Consulte aquí para obtener una explicación: Solidity Docs.

Finalmente, lo que realmente quiero discutir son los modificadores personalizados. Eche un vistazo a la línea 61 de BoxingOracle.sol:

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

Tenga en cuenta el modificador onlyOwner justo antes de la palabra clave "public". ¡Esto indica que solo el propietario del contrato puede llamar a este método! Si bien es muy importante, esta no es una característica nativa de Solidity (aunque tal vez lo sea en el futuro). En realidad, onlyOwner es un ejemplo de un modificador personalizado que creamos nosotros mismos y usamos. Echemos un vistazo.

Primero, el modificador se define en el archivo Ownable.sol, que puede ver que hemos importado en la línea 3 de BoxingOracle.sol:

 import "./Ownable.sol"

Tenga en cuenta que, para hacer uso del modificador, hemos hecho que BoxingOracle herede de Ownable . Dentro de Ownable.sol, en la línea 25, podemos encontrar la definición del modificador dentro del contrato “Ownable”:

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

(Este contrato Ownable, por cierto, está tomado de uno de los contratos públicos de OpenZeppelin).

Tenga en cuenta que esta cosa se declara como un modificador, lo que indica que podemos usarlo como lo hemos hecho, para modificar una función. Tenga en cuenta que la esencia del modificador es una declaración de "requerimiento". Las declaraciones requeridas son como afirmaciones, pero no para la depuración. Si la condición de la declaración requerida falla, la función generará una excepción. Entonces, parafraseando esta declaración de "requerir":

 require(msg.sender == owner);

Podríamos decir que significa:

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

Y, de hecho, en Solidity 0.4.22 y superior, podemos agregar un mensaje de error a esa instrucción require:

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

Finalmente, en la línea de aspecto curioso:

 _;

El guión bajo es la abreviatura de "Aquí, ejecute el contenido completo de la función modificada". Entonces, en efecto, la instrucción require se ejecutará primero, seguida de la función real. Entonces es como anteponer esta línea de lógica a la función modificada.

Por supuesto, hay más cosas que puedes hacer con los modificadores. Consulte los documentos: Documentos.

Bibliotecas de solidez

Hay una característica de lenguaje de Solidity conocida como la biblioteca . Tenemos un ejemplo en nuestro proyecto en DateLib.sol.

¡Implementación de Solidity Library!

Esta es una biblioteca para un manejo más fácil de los tipos de fecha. Se importa a BoxingOracle en la línea 4:

 import "./DateLib.sol";

Y se usa en la línea 13:

 using DateLib for DateLib.DateTime;

DateLib.DateTime es una estructura que se exporta del contrato DateLib (se expone como un miembro; consulte la línea 4 de DateLib.sol) y aquí declaramos que estamos "usando" la biblioteca DateLib para un determinado tipo de datos. Entonces, los métodos y operaciones declarados en esa biblioteca se aplicarán al tipo de datos que dijimos que debería. Así es como se usa una biblioteca en Solidity.

Para ver un ejemplo más claro, consulte algunas de las bibliotecas de números de OpenZeppelin, como SafeMath. Estos se pueden aplicar a tipos de datos Solidity nativos (numéricos) (mientras que aquí hemos aplicado una biblioteca a un tipo de datos personalizado) y se usan ampliamente.

Interfaces

Como en los principales lenguajes orientados a objetos, se admiten las interfaces. Las interfaces en Solidity se definen como contratos, pero los cuerpos de las funciones se omiten para las funciones. Para ver un ejemplo de una definición de interfaz, consulte OracleInterface.sol. En este ejemplo, la interfaz se usa como sustituto del contrato de Oracle, cuyo contenido reside en un contrato separado con una dirección separada.

Convenciones de nombres

Por supuesto, las convenciones de nomenclatura no son una regla global; como programadores, sabemos que somos libres de seguir las convenciones de codificación y nomenclatura que nos atraigan. Por otro lado, queremos que los demás se sientan cómodos leyendo y trabajando con nuestro código, por lo que es deseable cierto grado de estandarización.

Descripción del proyecto

Entonces, ahora que hemos repasado algunas características generales del lenguaje presentes en los archivos de código en cuestión, podemos comenzar a analizar de manera más específica el código en sí, para este proyecto.

Entonces, aclaremos el propósito de este proyecto, una vez más. El propósito de este proyecto es proporcionar una demostración semirrealista (o pseudorrealista) y un ejemplo de un contrato inteligente que utiliza un oráculo. En el fondo, esto es solo un contrato que llama a otro contrato separado.

El caso de negocio del ejemplo puede enunciarse de la siguiente manera:

  • Un usuario quiere hacer apuestas de varios tamaños en combates de boxeo, pagando dinero (éter) por las apuestas y cobrando sus ganancias cuando y si ganan.
  • Un usuario hace estas apuestas a través de un contrato inteligente. (En un caso de uso de la vida real, sería una DApp completa con un front-end web3; pero solo estamos examinando el lado de los contratos).
  • Un contrato inteligente separado, el oráculo, es mantenido por un tercero. Su trabajo es mantener una lista de los combates de boxeo con sus estados actuales (pendientes, en progreso, terminados, etc.) y, si terminó, el ganador.
  • El contrato principal obtiene listas de partidos pendientes del oráculo y los presenta a los usuarios como partidos "apuestas".
  • El contrato principal acepta apuestas hasta el comienzo de un partido.
  • Después de que se decide un partido, el contrato principal divide las ganancias y las pérdidas de acuerdo con un algoritmo simple, toma una parte para sí mismo y paga las ganancias a pedido (los perdedores simplemente pierden toda su apuesta).

Las reglas de apuestas:

  • Hay una apuesta mínima definida (definida en wei).
  • No hay apuesta máxima; los usuarios pueden apostar cualquier cantidad que deseen por encima del mínimo.
  • Los usuarios pueden hacer apuestas hasta el momento en que el partido esté "en progreso".

Algoritmo para dividir las ganancias:

  • Todas las apuestas recibidas se colocan en un "bote".
  • Se saca un pequeño porcentaje de la olla, para la casa.
  • Cada ganador recibe una parte del bote, directamente proporcional al tamaño relativo de sus apuestas.
  • Las ganancias se calculan tan pronto como el primer usuario solicita los resultados, después de que se decide el partido.
  • Las ganancias se otorgan a pedido del usuario.
  • En caso de empate, nadie gana: todos recuperan su apuesta y la casa no se lleva ninguna parte.

BoxingOracle: el contrato de Oracle

Funciones principales proporcionadas

El oráculo tiene dos interfaces, se podría decir: una presentada al “propietario” y mantenedor del contrato y otra presentada al público en general; es decir, contratos que consumen el oráculo. El mantenedor ofrece funcionalidad para introducir datos en el contrato, esencialmente tomando datos del mundo exterior y colocándolos en la cadena de bloques. Al público, ofrece acceso de solo lectura a dichos datos. Es importante tener en cuenta que el contrato en sí mismo restringe a los no propietarios la edición de datos, pero el acceso de solo lectura a esos datos se otorga públicamente sin restricciones.

A los usuarios:

  • Listar todos los partidos
  • Lista de partidos pendientes
  • Obtener detalles de un partido específico
  • Obtener el estado y el resultado de un partido específico

al propietario:

  • Introduce una coincidencia
  • Cambiar el estado del partido
  • Establecer el resultado del partido

Ilustración de elementos de acceso de usuario y propietario

Historia del usuario:

  • Se anuncia y confirma un nuevo combate de boxeo para el 9 de mayo.
  • Yo, el mantenedor del contrato (quizás soy una red deportiva conocida o un nuevo medio), agrego el próximo partido a los datos del oráculo en la cadena de bloques, con el estado "pendiente". Cualquiera o cualquier contrato ahora puede consultar y usar estos datos como quiera.
  • Cuando comienza el partido, establezco el estado de ese partido en "en progreso".
  • Cuando finaliza el partido, establezco el estado del partido en "completado" y modifico los datos del partido para indicar el ganador.

Revisión de código de Oracle

Esta revisión se basa completamente en BoxingOracle.sol; los números de línea hacen referencia a ese archivo.

En las líneas 10 y 11, declaramos nuestro lugar de almacenamiento de fósforos:

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

matches es solo una matriz simple para almacenar instancias de coincidencias, y la asignación es solo una función para asignar una ID de coincidencia única (un valor de bytes32) a su índice en la matriz, de modo que si alguien nos entrega una ID sin procesar de una coincidencia, podemos use este mapeo para localizarlo.

En la línea 17, nuestra estructura de coincidencias se define y explica:

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

Línea 61: la función addMatch es para uso exclusivo del propietario del contrato; permite agregar una nueva coincidencia a los datos almacenados.

Línea 80: la función declareOutcome permite al propietario del contrato establecer un partido como "decidido", estableciendo el participante que ganó.

Líneas 102-166: Las siguientes funciones son todas invocables por el público. Estos son los datos de solo lectura que están abiertos al público en general:

  • La función getPendingMatches devuelve una lista de ID de todas las coincidencias cuyo estado actual es "pendiente".
  • La función getAllMatches devuelve una lista de ID de todas las coincidencias.
  • La función getMatch devuelve los detalles completos de una sola coincidencia, especificada por ID.

Las líneas 193-204 declaran funciones que son principalmente para pruebas, depuración y diagnóstico.

  • La función testConnection solo prueba que podemos llamar al contrato.
  • La función getAddress devuelve la dirección de este contrato.
  • La función addTestData agrega un montón de coincidencias de prueba a la lista de coincidencias.

Siéntase libre de explorar un poco el código antes de pasar a los siguientes pasos. Sugiero ejecutar el contrato de Oracle nuevamente en modo de depuración (como se describe en la Parte 1 de esta serie), llamar a diferentes funciones y examinar los resultados.

BoxingBets: El contrato del cliente

Es importante definir de qué es responsable el contrato del cliente (el contrato de apuestas) y de qué no es responsable. El contrato del cliente no es responsable de mantener listas de combates de boxeo reales ni de declarar sus resultados. Nosotros “confiamos” (sí, lo sé, hay esa palabra sensible, oh, oh, discutiremos esto en la Parte 3) en el oráculo para ese servicio. El contrato del cliente es responsable de aceptar apuestas. Es responsable del algoritmo que reparte las ganancias y las transfiere a las cuentas de los ganadores en función del resultado del partido (como se recibe del oráculo).

Además, todo está basado en pull y no hay eventos ni push. El contrato extrae datos del oráculo. El contrato extrae el resultado del partido del oráculo (en respuesta a la solicitud del usuario) y el contrato calcula las ganancias y las transfiere en respuesta a la solicitud del usuario.

Funciones principales proporcionadas

  • Listar todos los partidos pendientes
  • Obtener detalles de un partido específico
  • Obtener el estado y el resultado de un partido específico
  • Hacer una apuesta
  • Solicitar/recibir ganancias

Revisión del código del cliente

Esta revisión se basa completamente en BoxingBets.sol; los números de línea hacen referencia a ese archivo.

Las líneas 12 y 13, las primeras líneas de código del contrato, definen algunas asignaciones en las que almacenaremos los datos de nuestro contrato.

La línea 12 asigna direcciones de usuario a listas de ID. Esto es asignar un usuario a una lista de ID de apuestas que pertenecen al usuario. Entonces, para cualquier dirección de usuario dada, podemos obtener rápidamente una lista de todas las apuestas que ha realizado ese usuario.

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

La línea 13 asigna la identificación única de un partido a una lista de instancias de apuesta. Con esto, podemos, para cualquier partido dado, obtener una lista de todas las apuestas que se han realizado para ese partido.

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

Las líneas 17 y 18 están relacionadas con la conexión con nuestro oráculo. Primero, en la variable boxingOracleAddr , almacenamos la dirección del contrato de Oracle (establecido en cero de forma predeterminada). Podríamos codificar la dirección del oráculo, pero nunca podríamos cambiarla. (No poder cambiar la dirección del oráculo podría ser algo bueno o malo; podemos hablar de eso en la Parte 3). La siguiente línea crea una instancia de la interfaz de Oracle (que se define en OracleInterface.sol) y la almacena en una variable.

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

Si salta a la línea 58, verá la función setOracleAddress , en la que se puede cambiar esta dirección de Oracle y en la que se vuelve a crear una instancia de boxingOracle con una nueva dirección.

La línea 21 define nuestro tamaño mínimo de apuesta, en wei. Por supuesto, esto es en realidad una cantidad muy pequeña, solo 0.000001 éter.

 uint internal minimumBet = 1000000000000;

En las líneas 58 y 66 respectivamente, tenemos las setOracleAddress y getOracleAddress . setOracleAddress tiene el modificador onlyOwner porque solo el propietario del contrato puede cambiar el oráculo por otro oráculo (probablemente no sea una buena idea, pero lo explicaremos en la Parte 3). La función getOracleAddress , por otro lado, se puede llamar públicamente; cualquiera puede ver qué oráculo se está utilizando.

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

En las líneas 72 y 79 tenemos las funciones getBettableMatches y getMatch , respectivamente. Tenga en cuenta que estos simplemente reenvían las llamadas al oráculo y devuelven el resultado.

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

La función placeBet es muy importante (línea 108).

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

Una característica llamativa de este es el modificador payable ; ¡Hemos estado tan ocupados discutiendo las características generales del idioma que aún no hemos tocado la característica centralmente importante de poder enviar dinero junto con llamadas de función! Eso es básicamente lo que es: es una función que puede aceptar una cantidad de dinero junto con cualquier otro argumento y datos enviados.

Necesitamos esto aquí porque aquí es donde el usuario define simultáneamente qué apuesta va a hacer, cuánto dinero pretende tener en esa apuesta y, de hecho, envía el dinero. El modificador payable permite eso. Antes de aceptar la apuesta, hacemos una serie de comprobaciones para garantizar la validez de la apuesta. La primera verificación en la línea 111 es:

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

La cantidad de dinero enviada se almacena en msg.value . Suponiendo que pasen todos los cheques, en la línea 123, transferiremos esa cantidad a la propiedad del oráculo, quitando la propiedad de esa cantidad al usuario y a la posesión del contrato:

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

Finalmente, en la línea 136, tenemos una función auxiliar de prueba/depuración que nos ayudará a saber si el contrato está conectado o no a un oráculo válido:

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

Terminando

Y esto es en realidad hasta donde llega este ejemplo; simplemente aceptando la apuesta. La funcionalidad para dividir las ganancias y pagar, así como alguna otra lógica, se omitió intencionalmente para mantener el ejemplo lo suficientemente simple para nuestro propósito, que es simplemente demostrar el uso de un oráculo con un contrato. Esa lógica más completa y compleja existe actualmente en otro proyecto, que es una extensión de este ejemplo y aún está en desarrollo.

Así que ahora tenemos una mejor comprensión del código base y lo hemos usado como vehículo y punto de partida para analizar algunas de las características del lenguaje que ofrece Solidity. El propósito principal de esta serie de tres partes es demostrar y discutir el uso de un contrato con un oráculo. El propósito de esta parte es comprender un poco mejor este código específico y usarlo como punto de partida para comprender algunas características de Solidity y el desarrollo de contratos inteligentes. El propósito de la tercera y última parte será discutir la estrategia y la filosofía del uso de Oracle y cómo encaja conceptualmente en el modelo de contrato inteligente.

Otros pasos opcionales

Recomiendo encarecidamente a los lectores que deseen aprender más, que tomen este código y jueguen con él. Implementar nuevas características. Solucione cualquier error. Implemente funciones no implementadas (como la interfaz de pago). Pruebe las llamadas a funciones. Modifíquelos y vuelva a probar para ver qué sucede. Agregue un front-end web3. Agregue una función para eliminar coincidencias o modificar sus resultados (en caso de error). ¿Qué pasa con los partidos cancelados? Implementar un segundo oráculo. Por supuesto, un contrato es libre de usar tantos oráculos como quiera, pero ¿en qué problemas incurre eso? Diviértete con eso; esa es una gran manera de aprender, y cuando lo haces de esa manera (y disfrutas de ello) seguro que retienes más de lo que has aprendido.

Una muestra, lista no exhaustiva de cosas para probar:

  • Ejecute tanto el contrato como el oráculo en la red de prueba local (en trufa, como se describe en la Parte 1) y llame a todas las funciones invocables y todas las funciones de prueba.
  • Agregue funcionalidad para calcular las ganancias y pagarlas al finalizar un partido.
  • Agregue funcionalidad para reembolsar todas las apuestas en caso de empate.
  • Agregue una función para solicitar un reembolso o cancelar una apuesta, antes de que comience el partido.
  • Agregue una función para permitir que los partidos a veces se cancelen (en cuyo caso, todos necesitarán un reembolso).
  • Implemente una función para garantizar que el oráculo que estaba en su lugar cuando un usuario hizo una apuesta sea el mismo oráculo que se usará para determinar el resultado de ese partido.
  • Implemente otro (segundo) oráculo, que tenga algunas características diferentes asociadas, o posiblemente sirva para un deporte que no sea el boxeo (tenga en cuenta que los participantes cuentan y la lista permite diferentes tipos de deportes, por lo que en realidad no estamos restringidos solo al boxeo) .
  • Implemente getMostRecentMatch para que realmente devuelva la coincidencia agregada más recientemente o la coincidencia más cercana a la fecha actual en términos de cuándo ocurrirá.
  • Implementar el manejo de excepciones.

Una vez que esté familiarizado con la mecánica de la relación entre el contrato y el oráculo, en la Parte 3 de esta serie de tres partes, analizaremos algunas de las cuestiones estratégicas, de diseño y filosóficas que plantea este ejemplo.