Контракты Ethereum Oracle: особенности кода Solidity
Опубликовано: 2022-03-11В первом сегменте этого раздела, состоящего из трех частей, мы прошли небольшое руководство, которое дало нам простую пару контрактов с оракулом. Описаны механизмы и процессы настройки (с трюфелем), компиляции кода, развертывания в тестовой сети, запуска и отладки; однако многие детали кода были замазаны волнистыми руками. Итак, теперь, как и было обещано, мы рассмотрим некоторые из тех языковых функций, которые уникальны для разработки смарт-контрактов Solidity и уникальны для этого конкретного сценария контракт-оракул. Хотя мы не можем тщательно рассмотреть каждую деталь (если хотите, я оставлю это вам в ваших дальнейших исследованиях), мы постараемся нащупать самые яркие, самые интересные и самые важные особенности кода.
Чтобы облегчить это, я рекомендую вам открыть либо собственную версию проекта (если она у вас есть), либо иметь код под рукой для справки.
Полный код на данный момент можно найти здесь: https://github.com/jrkosinski/oracle-example/tree/part2-step1.
Эфириум и солидность
Solidity — не единственный доступный язык разработки смарт-контрактов, но я думаю, можно с уверенностью сказать, что это самый распространенный и популярный в целом для смарт-контрактов Ethereum. Конечно, это тот, который имеет самую популярную поддержку и информацию на момент написания этой статьи.
Solidity является объектно-ориентированным и полным по Тьюрингу. Тем не менее, вы быстро поймете его встроенные (и полностью преднамеренные) ограничения, из-за которых программирование смарт-контрактов сильно отличается от обычного взлома по принципу «давайте сделаем это».
Версия солидности
Вот первая строка каждого стихотворения о коде Solidity:
pragma solidity ^0.4.17;
Номера версий, которые вы видите, будут отличаться, так как Solidity, все еще находящаяся в молодости, быстро меняется и развивается. Версия 0.4.17 — это версия, которую я использовал в своих примерах; последняя версия на момент публикации — 0.4.25.
Последняя версия на данный момент, когда вы читаете это, вполне может быть чем-то совершенно другим. Многие полезные функции находятся в разработке (или, по крайней мере, запланированы) для Solidity, которые мы сейчас обсудим.
Вот обзор различных версий Solidity.
Совет для профессионалов: вы также можете указать диапазон версий (хотя я не вижу, чтобы это делалось слишком часто), например так:
pragma solidity >=0.4.16 <0.6.0;
Возможности языка программирования Solidity
В Solidity есть много языковых функций, знакомых большинству современных программистов, а также некоторые отличительные и (по крайней мере, для меня) необычные. Говорят, что он был вдохновлен C++, Python и JavaScript — все они хорошо знакомы мне лично, и все же Solidity кажется совершенно отличным от любого из этих языков.
Договор
Файл .sol — это основная единица кода. В BoxingOracle.sol обратите внимание на 9-ю строку:
contract BoxingOracle is Ownable {
Поскольку класс является базовой единицей логики в объектно-ориентированных языках, контракт является базовой единицей логики в Solidity. Достаточно пока упростить, сказав, что контракт — это «класс» Solidity (для объектно-ориентированных программистов это легкий прыжок).
Наследование
Контракты Solidity полностью поддерживают наследование, и оно работает так, как вы ожидаете; члены частного контракта не наследуются, в то время как защищенные и публичные наследуются. Перегрузка и полиморфизм поддерживаются, как и следовало ожидать.
contract BoxingOracle is Ownable {
В приведенном выше утверждении ключевое слово «есть» обозначает наследование. BoxingOracle наследует от Ownable. Множественное наследование также поддерживается в Solidity. Множественное наследование обозначается списком имен классов, разделенных запятыми, например:
contract Child is ParentA, ParentB, ParentC { …
Хотя (на мой взгляд) не стоит слишком усложнять структуру вашей модели наследования, вот интересная статья о Solidity в отношении так называемой алмазной проблемы.
перечисления
Перечисления поддерживаются в 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 }
Как и следовало ожидать (не отличается от знакомых языков), каждому значению перечисления присваивается целочисленное значение, начинающееся с 0. Как указано в документации Solidity, значения перечисления могут быть преобразованы во все целочисленные типы (например, uint, uint16, uint32, д.), но неявное преобразование не допускается. Это означает, что они должны приводиться явно (например, к uint).
Документы Solidity: Enums Учебник по Enums
Структуры
Структуры — это еще один способ, как и перечисления, для создания определяемого пользователем типа данных. Структуры знакомы всем программистам, работающим с основами C/C++, и таким старым ребятам, как я. Пример структуры из строки 17 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; }
Примечание для всех старых программистов на C: «упаковка» структур в Solidity — это вещь, но есть некоторые правила и предостережения. Не обязательно предполагать, что он работает так же, как в C; проверьте документы и будьте в курсе вашей ситуации, чтобы убедиться, поможет ли вам упаковка в данном случае.
Упаковка структуры прочности
После создания к структурам можно обращаться в коде как к родным типам данных. Вот пример синтаксиса для «экземпляра» типа структуры, созданного выше:
Match match = Match(id, "A vs. B", "A|B", 2, block.timestamp, MatchOutcome.Pending, 1);
Типы данных в Solidity
Это подводит нас к самой основной теме типов данных в Solidity. Какие типы данных поддерживает Solidity? Solidity имеет статическую типизацию, и на момент написания этой статьи типы данных должны быть явно объявлены и привязаны к переменным.
Типы данных Solidity
Булевы значения
Логические типы поддерживаются под именем bool и значениями true или false .
Числовые типы
Поддерживаются целые типы, как знаковые, так и беззнаковые, от int8/uint8 до int256/uint256 (от 8-битных целых до 256-битных целых чисел соответственно). Тип uint является сокращением от uint256 (и аналогично int является сокращением от int256).
Примечательно, что типы с плавающей запятой не поддерживаются. Почему бы нет? Ну, во-первых, хорошо известно, что при работе с денежными значениями переменные с плавающей запятой — плохая идея (в общем, конечно), потому что значение может раствориться в воздухе. Значения эфира обозначаются в wei, что составляет 1/1 000 000 000 000 000 000 эфира, и этой точности должно быть достаточно для всех целей; вы не можете разбить эфир на более мелкие части.
В настоящее время значения с фиксированной точкой частично поддерживаются. Согласно документам Solidity: «Числа с фиксированной точкой еще не полностью поддерживаются Solidity. Они могут быть объявлены, но не могут быть назначены или от них».
https://hackernoon.com/a-note-on-numbers-in-ethereum-and-javascript-3e6ac3b2fad9
Примечание. В большинстве случаев лучше просто использовать uint, так как уменьшение размера переменной (например, до uint32) может фактически увеличить затраты на газ, а не уменьшить их, как можно было ожидать. Как правило, используйте uint, если только вы не уверены, что у вас есть веская причина поступать иначе.
Типы строк
Строковый тип данных в Solidity — забавная тема; вы можете получить разные мнения в зависимости от того, с кем вы разговариваете. В Solidity есть строковый тип данных, это факт. Мое мнение, вероятно, разделяемое большинством, заключается в том, что он не предлагает много функций. Разбор строк, объединение, замена, обрезка, даже подсчет длины строки: ничего из того, что вы, вероятно, ожидаете от строкового типа, не присутствует, и поэтому вы несете ответственность за них (если они вам нужны). Некоторые используют bytes32 вместо строки; это тоже можно сделать.
Забавная статья о струнах Solidity
Мое мнение: может быть забавным упражнением написать свой собственный строковый тип и опубликовать его для общего пользования.
тип адреса
Уникальный, возможно, для Solidity, у нас есть тип данных адреса , специально для кошелька Ethereum или адресов контрактов. Это 20-байтовое значение специально для хранения адресов такого размера. Кроме того, у него есть члены типа специально для таких адресов.
address internal boxingOracleAddr = 0x145ca3e014aaf5dca488057592ee45305d9b3a22;
Типы адресных данных
Типы даты и времени
В Solidity нет родного типа Date или DateTime как такового, как, например, в JavaScript. (О нет — Solidity звучит все хуже и хуже с каждым абзацем!?) Даты изначально адресуются как метки времени типа uint (uint256). Обычно они обрабатываются как метки времени в стиле Unix, в секундах, а не в миллисекундах, поскольку метка времени блока является меткой времени в стиле Unix. В тех случаях, когда вам нужны удобочитаемые даты по разным причинам, доступны библиотеки с открытым исходным кодом. Вы могли заметить, что я использовал один из них в BoxingOracle: DateLib.sol. В OpenZeppelin также есть утилиты для работы с датами, а также многие другие типы общих служебных библиотек (мы скоро перейдем к библиотечной функции Solidity).
Совет для профессионалов: OpenZeppelin — хороший источник (но, конечно, не единственный хороший источник) как знаний, так и предварительно написанного универсального кода, который может помочь вам в построении ваших контрактов.
Сопоставления
Обратите внимание, что строка 11 BoxingOracle.sol определяет то, что называется сопоставлением :
mapping(bytes32 => uint) matchIdToIndex;
Отображение в Solidity — это специальный тип данных для быстрого поиска; по сути, таблица поиска или похожая на хеш-таблицу, в которой содержащиеся данные живут в самой цепочке блоков (когда сопоставление определено, как здесь, как член класса). В ходе выполнения контракта мы можем добавлять данные в сопоставление, аналогично добавлению данных в хеш-таблицу, а затем искать эти значения, которые мы добавили. Еще раз обратите внимание, что в этом случае данные, которые мы добавляем, добавляются в саму цепочку блоков, поэтому они будут сохраняться. Если мы добавим его на карту сегодня в Нью-Йорке, через неделю кто-нибудь в Стамбуле сможет его прочитать.
Пример добавления к отображению из строки 71 BoxingOracle.sol:
matchIdToIndex[id] = newIndex+1
Пример чтения из сопоставления из строки 51 BoxingOracle.sol:
uint index = matchIdToIndex[_matchId];
Элементы также могут быть удалены из сопоставления. В этом проекте он не используется, но будет выглядеть так:
delete matchIdToIndex[_matchId];
Возвращаемые значения
Как вы, возможно, заметили, Solidity может иметь некоторое внешнее сходство с Javascript, но он не наследует большую часть свободы типов и определений JavaScript. Код контракта должен быть определен довольно строгим и ограниченным образом (и это, вероятно, хорошо, учитывая вариант использования). Имея это в виду, рассмотрим определение функции из строки 40 BoxingOracle.sol.
function _getMatchIndex(bytes32 _matchId) private view returns (uint) { ... }
Итак, давайте сначала сделаем краткий обзор того, что здесь содержится. function
помечает его как функцию. _getMatchIndex
— это имя функции (подчеркивание — это соглашение, указывающее на закрытый член — мы обсудим это позже). Он принимает один аргумент с именем _matchId
(на этот раз для обозначения аргументов функции используется соглашение о подчеркивании) типа bytes32
. Ключевое слово private
фактически делает член закрытым в области видимости, view
сообщает компилятору, что эта функция не изменяет никаких данных в цепочке блоков, и, наконец: ~~~ Solidity возвращает (uint) ~~~
Это говорит о том, что функция возвращает uint (функция, returns
void, просто не будет иметь здесь пункта return). Почему uint в скобках? Это потому, что функции Solidity могут и часто возвращают кортежи .
Рассмотрим теперь следующее определение из строки 166:
function getMostRecentMatch(bool _pending) public view returns ( bytes32 id, string name, string participants, uint8 participantCount, uint date, MatchOutcome outcome, int8 winner) { ... }
Проверьте пункт возврата на этом! Он возвращает одну, две… семь разных вещей. Итак, эта функция возвращает эти вещи в виде кортежа. Почему? В ходе разработки вам часто придется возвращать структуру (если бы это был JavaScript, вы, вероятно, хотели бы вернуть объект JSON). Что ж, на момент написания этой статьи (хотя в будущем это может измениться) Solidity не поддерживает возврат структур из публичных функций. Поэтому вместо этого вы должны возвращать кортежи. Если вы разбираетесь в Python, возможно, вам уже удобно работать с кортежами. Однако многие языки на самом деле не поддерживают их, по крайней мере, не таким образом.
См. строку 159 для примера возврата кортежа в качестве возвращаемого значения:
return (_matchId, "", "", 0, 0, MatchOutcome.Pending, -1);
И как мы принимаем возвращаемое значение чего-то подобного? Мы можем сделать так:
var (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
В качестве альтернативы вы можете заранее явно объявить переменные с их правильными типами:
//declare the variables bytes32 id; string name; ... etc... int8 winner; //assign their values (id, name, part, count, date, outcome, winner) = getMostRecentMatch(false);
И теперь мы объявили 7 переменных для хранения 7 возвращаемых значений, которые мы теперь можем использовать. В противном случае, предположив, что нам нужно только одно или два значения, мы можем сказать:
//declare the variables bytes32 id; uint date; //assign their values (id,,,,date,,) = getMostRecentMatch(false);
Видишь, что мы там делали? Мы получили только два, которые нас интересовали. Проверьте все эти запятые. Мы должны тщательно их пересчитать!
Импорт
Строки 3 и 4 BoxingOracle.sol являются импортом:
import "./Ownable.sol"; import "./DateLib.sol";
Как и следовало ожидать, это импорт определений из файлов кода, находящихся в той же папке проекта контрактов, что и BoxingOracle.sol.
Модификаторы
Обратите внимание, что к определениям функций прикреплено множество модификаторов. Во-первых, это видимость: частная, публичная, внутренняя и внешняя — видимость функций.
Кроме того, вы увидите ключевые слова pure
и view
. Они указывают компилятору, какие изменения будет производить функция, если таковые имеются. Это важно, потому что такая вещь является фактором, влияющим на конечную стоимость газа для запуска функции. Объяснение смотрите здесь: Solidity Docs.
Наконец, что я действительно хочу обсудить, так это пользовательские модификаторы. Взгляните на строку 61 BoxingOracle.sol:
function addMatch(string _name, string _participants, uint8 _participantCount, uint _date) onlyOwner public returns (bytes32) {
Обратите внимание на модификатор onlyOwner
непосредственно перед ключевым словом «public». Это указывает на то, что только владелец контракта может вызывать этот метод! Хотя это очень важно, это не встроенная функция Solidity (хотя, возможно, она будет в будущем). На самом деле onlyOwner
— это пример пользовательского модификатора, который мы создаем сами и используем. Давайте посмотрим.
Во-первых, модификатор определен в файле Ownable.sol, который, как вы можете видеть, мы импортировали в строке 3 BoxingOracle.sol:
import "./Ownable.sol"
Обратите внимание, что для использования модификатора мы сделали BoxingOracle
наследником Ownable
. Внутри Ownable.sol, в строке 25, мы можем найти определение модификатора внутри контракта «Ownable»:
modifier onlyOwner() { require(msg.sender == owner); _; }
(Этот контракт Ownable, кстати, взят из одного из публичных контрактов OpenZeppelin.)
Обратите внимание, что эта вещь объявлена как модификатор, указывающий, что мы можем использовать ее, как у нас есть, для изменения функции. Обратите внимание, что ядром модификатора является оператор «require». Операторы Require похожи на утверждения, но не для отладки. Если условие оператора require не выполняется, функция выдает исключение. Итак, перефразируя это «требуемое» утверждение:
require(msg.sender == owner);
Мы могли бы сказать, что это означает:
if (msg.send != owner) throw an exception;
И действительно, в Solidity 0.4.22 и выше мы можем добавить сообщение об ошибке в этот оператор require:
require(msg.sender == owner, "Error: this function is callable by the owner of the contract, only");
Наконец, в любопытной строчке:

_;
Подчеркивание — это сокращение от «Здесь выполните полное содержимое измененной функции». Таким образом, оператор require будет выполняться первым, а затем фактическая функция. Так что это похоже на добавление этой строки логики в модифицированную функцию.
Конечно, с модификаторами можно делать и другие вещи. Проверьте документы: Документы.
Библиотеки Solidity
В Solidity есть языковая функция, известная как библиотека . У нас есть пример в нашем проекте DateLib.sol.
Это библиотека для более удобной обработки типов дат. Он импортируется в BoxingOracle в строке 4:
import "./DateLib.sol";
И он используется в строке 13:
using DateLib for DateLib.DateTime;
DateLib.DateTime
— это структура, которая экспортируется из контракта DateLib (она предоставляется как член; см. строку 4 DateLib.sol), и здесь мы объявляем, что мы «используем» библиотеку DateLib для определенного типа данных. Таким образом, методы и операции, объявленные в этой библиотеке, будут применяться к указанному типу данных. Вот как библиотека используется в Solidity.
Чтобы получить более наглядный пример, ознакомьтесь с некоторыми библиотеками OpenZeppelin для работы с числами, такими как SafeMath. Они могут быть применены к собственным (числовым) типам данных Solidity (тогда как здесь мы применили библиотеку к пользовательскому типу данных) и широко используются.
Интерфейсы
Как и в основных объектно-ориентированных языках, поддерживаются интерфейсы. Интерфейсы в Solidity определены как контракты, но тела функций опущены. Пример определения интерфейса см. в OracleInterface.sol. В этом примере интерфейс используется в качестве замены для контракта оракула, содержимое которого находится в отдельном контракте с отдельным адресом.
Соглашения об именах
Конечно, соглашения об именах не являются глобальным правилом; как программисты, мы знаем, что можем свободно следовать соглашениям о кодировании и именовании, которые нам нравятся. С другой стороны, мы хотим, чтобы другим было удобно читать наш код и работать с ним, поэтому желательна некоторая степень стандартизации.
Обзор проекта
Итак, теперь, когда мы рассмотрели некоторые общие особенности языка, присутствующие в рассматриваемых файлах кода, мы можем начать более конкретно рассматривать сам код для этого проекта.
Итак, давайте еще раз уточним цель этого проекта. Цель этого проекта — предоставить полуреалистичную (или псевдореалистичную) демонстрацию и пример смарт-контракта, использующего оракул. По сути, это просто контракт, вызывающий другой отдельный контракт.
Бизнес-кейс примера можно сформулировать следующим образом:
- Пользователь хочет делать ставки разных размеров на боксерские поединки, платя деньги (эфир) за ставки и получая свой выигрыш, когда и если они выиграют.
- Пользователь делает эти ставки через смарт-контракт. (В реальном случае это будет полноценное DApp с интерфейсом web3, но мы рассматриваем только контрактную сторону.)
- Отдельный смарт-контракт — оракул — поддерживается третьей стороной. Его задача состоит в том, чтобы поддерживать список боксерских поединков с их текущими состояниями (ожидание, в процессе, завершение и т. д.) и, если они завершены, победителя.
- Основной контракт получает списки ожидающих совпадений от оракула и представляет их пользователям как совпадения, на которые можно сделать ставку.
- Основной контракт принимает ставки до начала матча.
- После решения матча основной контракт делит выигрыш и проигрыш по простому алгоритму, берет себе долю и выплачивает выигрыш по запросу (проигравшие просто теряют всю свою ставку).
Правила ставок:
- Существует определенная минимальная ставка (определяется в вей).
- Максимальная ставка отсутствует; пользователи могут делать ставки на любую сумму сверх минимальной.
- Пользователи могут делать ставки до тех пор, пока матч не станет «идет».
Алгоритм разделения выигрыша:
- Все полученные ставки помещаются в «банк».
- Небольшой процент снимается с горшка, для дома.
- Каждый победитель получает часть банка, прямо пропорциональную относительному размеру его ставок.
- Выигрыши рассчитываются, как только самый первый пользователь запрашивает результаты, после того как матч решен.
- Выигрыши начисляются по запросу пользователя.
- В случае ничьей никто не выигрывает — каждый получает свою ставку обратно, и казино не получает долю.
BoxingOracle: контракт оракула
Основные функции
Можно сказать, что у оракула есть два интерфейса: один представлен «владельцу» и сопровождающему контракту, а другой — широкой публике; то есть контракты, которые потребляют оракула. Сопровождающий предлагает функциональные возможности для ввода данных в контракт, по сути, беря данные из внешнего мира и помещая их в блокчейн. Для общественности он предлагает доступ только для чтения к указанным данным. Важно отметить, что сам контракт запрещает лицам, не являющимся владельцами, редактировать какие-либо данные, но доступ только для чтения к этим данным предоставляется публично без ограничений.
Пользователям:
- Список всех совпадений
- Список ожидающих совпадений
- Получить подробную информацию о конкретном матче
- Получить статус и результат конкретного матча
Владельцу:
- Введите совпадение
- Изменить статус матча
- Установить исход матча
История пользователя:
- Новый боксерский поединок объявлен и подтвержден на 9 мая.
- Я, сопровождающий контракт (возможно, я известная спортивная сеть или новая торговая точка), добавляю предстоящий матч в данные оракула в блокчейне со статусом «ожидание». Теперь любой или любой контракт может запрашивать и использовать эти данные по своему усмотрению.
- Когда матч начинается, я устанавливаю статус этого матча на «в процессе».
- Когда матч заканчивается, я устанавливаю статус матча на «завершен» и изменяю данные матча, чтобы обозначить победителя.
Обзор кода Oracle
Этот обзор полностью основан на BoxingOracle.sol; номера строк ссылаются на этот файл.
В строках 10 и 11 мы объявляем место хранения спичек:
Match[] matches; mapping(bytes32 => uint) matchIdToIndex;
matches
— это просто простой массив для хранения экземпляров совпадений, а сопоставление — это просто средство для сопоставления уникального идентификатора совпадения (значение bytes32) с его индексом в массиве, чтобы, если кто-то передаст нам необработанный идентификатор совпадения, мы могли используйте это сопоставление, чтобы найти его.
В строке 17 определена и объяснена наша структура соответствия:
//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 }
Строка 61: Функция addMatch
предназначена для использования только владельцем контракта; это позволяет добавить новое совпадение к сохраненным данным.
Строка 80: функция declareOutcome
позволяет владельцу контракта установить матч как «решенный», установив участника, который выиграл.
Строки 102-166: Все следующие функции доступны для общего доступа. Это данные только для чтения, которые в целом открыты для публики:
- Функция
getPendingMatches
возвращает список идентификаторов всех совпадений, текущее состояние которых «ожидание». - Функция
getAllMatches
возвращает список идентификаторов всех совпадений. - Функция
getMatch
возвращает полную информацию об одном совпадении, заданном идентификатором.
Строки 193-204 объявляют функции, предназначенные в основном для тестирования, отладки и диагностики.
- Функция
testConnection
просто проверяет, что мы можем вызвать контракт. - Функция
getAddress
возвращает адрес этого контракта. - Функция
addTestData
добавляет в список совпадений кучу тестовых совпадений.
Не стесняйтесь немного изучить код, прежде чем переходить к следующим шагам. Я предлагаю снова запустить контракт оракула в режиме отладки (как описано в первой части этой серии), вызвать различные функции и изучить результаты.
BoxingBets: Клиентский контракт
Важно определить, за что отвечает клиентский договор (букмекерский договор), а за что нет. Клиентский контракт не несет ответственности за ведение списков реальных боксерских поединков или объявление их результатов. Мы «доверяем» (да, я знаю, это деликатное слово — о, о, мы обсудим это в части 3) оракулу для этой службы. Клиентский договор отвечает за прием ставок. Он отвечает за алгоритм, который делит выигрыши и переводит их на счета победителей в зависимости от исхода матча (полученного от оракула).
Кроме того, все основано на вытягивании, и нет никаких событий или толчков. Контракт извлекает данные из оракула. Контракт получает результат матча от оракула (в ответ на запрос пользователя), а контракт подсчитывает выигрыши и переводит их в ответ на запрос пользователя.
Основные функции
- Список всех ожидающих совпадений
- Получить подробную информацию о конкретном матче
- Получить статус и результат конкретного матча
- Сделать ставку
- Запросить/получить выигрыш
Обзор клиентского кода
Этот обзор полностью основан на BoxingBets.sol; номера строк ссылаются на этот файл.
Строки 12 и 13, первые строки кода в контракте, определяют некоторые сопоставления, в которых мы будем хранить данные нашего контракта.
Строка 12 сопоставляет адреса пользователей спискам идентификаторов. Это сопоставление пользователя со списком идентификаторов ставок, которые принадлежат пользователю. Таким образом, для любого заданного адреса пользователя мы можем быстро получить список всех ставок, сделанных этим пользователем.
mapping(address => bytes32[]) private userToBets;
В строке 13 уникальный идентификатор матча сопоставляется со списком экземпляров ставок. При этом мы можем для любого заданного матча получить список всех ставок, сделанных на этот матч.
mapping(bytes32 => Bet[]) private matchToBets;
Строки 17 и 18 связаны с подключением к нашему оракулу. Во-первых, в переменной boxingOracleAddr
мы храним адрес контракта оракула (по умолчанию установлен равным нулю). Мы могли бы жестко запрограммировать адрес оракула, но тогда мы никогда не смогли бы его изменить. (Невозможность изменить адрес оракула может быть как хорошей, так и плохой вещью — мы можем обсудить это в части 3). Следующая строка создает экземпляр интерфейса оракула (который определен в OracleInterface.sol) и сохраняет его в переменной.
//boxing results oracle address internal boxingOracleAddr = 0; OracleInterface internal boxingOracle = OracleInterface(boxingOracleAddr);
Если вы перейдете к строке 58, вы увидите функцию setOracleAddress
, в которой этот адрес оракула может быть изменен, и в которой экземпляр boxingOracle
повторно создается с новым адресом.
Строка 21 определяет размер нашей минимальной ставки в вэй. Это, конечно, очень небольшое количество, всего 0,000001 эфира.
uint internal minimumBet = 1000000000000;
В строках 58 и 66 соответственно у нас есть функции setOracleAddress
и getOracleAddress
. setOracleAddress
имеет модификатор onlyOwner
, потому что только владелец контракта может заменить оракула другим оракулом (вероятно, не очень хорошая идея, но мы подробнее остановимся на ней в части 3). С другой стороны, функцию getOracleAddress
публично; любой может видеть, какой оракул используется.
function setOracleAddress(address _oracleAddress) external onlyOwner returns (bool) {... function getOracleAddress() external view returns (address) { ....
В строках 72 и 79 у нас есть функции getBettableMatches
и getMatch
соответственно. Обратите внимание, что они просто перенаправляют вызовы оракулу и возвращают результат.
function getBettableMatches() public view returns (bytes32[]) {... function getMatch(bytes32 _matchId) public view returns ( ....
Функция placeBet
очень важна (строка 108).
function placeBet(bytes32 _matchId, uint8 _chosenWinner) public payable { ...
Яркой особенностью этого является payable
модификатор; мы были так заняты обсуждением общих возможностей языка, что еще не коснулись такой важной возможности, как возможность отправлять деньги вместе с вызовами функций! Это в основном то, что это такое — это функция, которая может принимать сумму денег вместе с любыми другими отправленными аргументами и данными.
Нам это нужно здесь, потому что здесь пользователь одновременно определяет, какую ставку он собирается сделать, сколько денег он собирается получить на этой ставке, и фактически отправляет деньги. Модификатор payable
разрешает это. Прежде чем принять ставку, мы делаем кучу проверок, чтобы убедиться в ее действительности. Первая проверка в строке 111:
require(msg.value >= minimumBet, "Bet amount must be >= minimum bet");
Сумма отправленных денег хранится в msg.value
. Предполагая, что все проверки пройдены, в строке 123 мы передадим эту сумму во владение оракула, отняв право собственности на эту сумму у пользователя и во владение контрактом:
address(this).transfer(msg.value);
Наконец, в строке 136 у нас есть вспомогательная функция тестирования/отладки, которая поможет нам узнать, связан ли контракт с действительным оракулом:
function testOracleConnection() public view returns (bool) { return boxingOracle.testConnection(); }
Подведение итогов
И это на самом деле то, что касается этого примера; просто принять ставку. Функционал по разделу выигрыша и выплате, а также некоторая другая логика были намеренно опущены, чтобы сделать пример достаточно простым для нашей цели, которая состоит в том, чтобы просто продемонстрировать использование оракула с контрактом. Эта более полная и сложная логика в настоящее время существует в другом проекте, который является расширением этого примера и все еще находится в разработке.
Итак, теперь мы лучше понимаем кодовую базу и использовали ее как средство и отправную точку для обсуждения некоторых языковых функций, предлагаемых Solidity. Основная цель этой серии из трех частей — продемонстрировать и обсудить использование контракта с оракулом. Цель этой части — немного лучше понять этот конкретный код и использовать его в качестве отправной точки для понимания некоторых особенностей Solidity и разработки смарт-контрактов. Целью третьей и последней части будет обсуждение стратегии и философии использования оракула и того, как он концептуально вписывается в модель смарт-контракта.
Дальнейшие необязательные шаги
Я настоятельно рекомендую читателям, желающим узнать больше, взять этот код и поиграть с ним. Внедряйте новые функции. Исправьте любые ошибки. Реализовать нереализованные функции (например, платежный интерфейс). Протестируйте вызовы функций. Измените их и повторите тестирование, чтобы увидеть, что произойдет. Добавьте интерфейс web3. Добавьте возможность удалять матчи или изменять их результаты (в случае ошибки). А отмененные матчи? Реализовать второго оракула. Конечно, контракт может использовать сколько угодно оракулов, но какие проблемы это влечет за собой? Получайте удовольствие от этого; это отличный способ учиться, и когда вы делаете это таким образом (и получаете от этого удовольствие), вы обязательно сохраните больше того, что вы узнали.
Примерный, неполный список вещей, которые стоит попробовать:
- Запустите и контракт, и оракул в локальной тестовой сети (в трюфе, как описано в части 1) и вызовите все вызываемые функции и все тестовые функции.
- Добавьте функционал для расчета выигрыша и его выплаты по завершению матча.
- Добавить функционал возврата всех ставок в случае ничьей.
- Добавьте функцию запроса возврата или отмены ставки до начала матча.
- Добавьте функцию, учитывающую тот факт, что иногда матчи могут быть отменены (в этом случае всем потребуется возврат средств).
- Реализуйте функцию, гарантирующую, что оракул, который был на месте, когда пользователь делал ставку, является тем же оракулом, который будет использоваться для определения исхода этого матча.
- Реализовать еще один (второй) оракул, который имеет некоторые другие функции, связанные с ним, или, возможно, служит для спорта, отличного от бокса (обратите внимание, что подсчет и список участников позволяют заниматься разными видами спорта, поэтому мы на самом деле не ограничены только боксом) .
-
getMostRecentMatch
так, чтобы он фактически возвращал либо последнее добавленное совпадение, либо совпадение, ближайшее к текущей дате с точки зрения того, когда оно произойдет. - Реализовать обработку исключений.
После того, как вы ознакомитесь с механикой отношений между контрактом и оракулом, в части 3 этой серии из трех частей мы обсудим некоторые стратегические, проектные и философские вопросы, поднятые в этом примере.