Всем привет! Меня зовут Степан, я руководитель группы разработки в Waves Enterprise, а конкретно — подразделения, которое создает децентрализованные приложения, реализующие процессы реального бизнеса на базе нашего блокчейна. В этом посте я хочу рассказать о нашем SDK для JVM-языков программирования, с помощью которого каждый Java/Kotlin-разработчик сможет попробовать себя в создании блокчейн-приложений. Чтобы развернуть реальную бизнес-логику на блокчейне, необходимы смарт-контракты. Помимо реализации этой логики, они выступают гарантом прозрачного и честного выполнения условий всеми участниками системы. Это необходимое условие для перехода к Web3 с его гарантированно честным и безопасным прямым взаимодействием между участниками сети. О том, как работают смарт-контракты в сети Waves Enterprise, вы можете узнать из поста моего коллеги Руслана. Более ранние проекты нашей компании использовали смарт-контракты, специально написанные под конкретную задачу, и зачастую содержали много шаблонного кода. Причина в том, что, вне зависимости от прикладной задачи, в коде всех docker смарт-контрактов есть одинаковая логика более низкого уровня:  подключение к ноде и получение данных транзакции, передача значений для сохранения на стейте контракта, обработка ошибок, преобразование доменных объектов для хранения на стейте контракта. После нескольких проектов с таким количеством шаблонного кода наш CTO Денис предложил выделить это в SDK смарт-контрактов. Заложенные в SDK подходы были основаны на опыте подобных решений в других блокчейн-сетях — Hyperledger, Ethereum. Впоследствии этот внутренний SDK хорошо показал себя во многих проектах. В 2022 году в рамках развития opensource-направления мы решили выложить этот SDK в общий доступ вместе с кодом ноды. Чтобы не было стыдно перед сообществом, мы отрефакторили код SDK и практически переписали его, чтобы решить ряд проблем. Далее я расскажу подробней о том, как устроен SDK, и приведу пример написанного на нем смарт-контракта. Все действия на смарт-контракте мы представляем как «методы» смарт-контракта. Эти «методы» соответствуют методам класса, реализующего данный смарт-контракт — класс помечается аннотацией ContractHandler. Методы могут принимать на вход параметры как примитивных (string, long, boolean), так и составных типов (по умолчанию сериализация в JSON). Тип и данные (блок params) транзакции смарт-контракта определяют, какой конкретно метод необходимо вызвать и какие конкретно параметры ему передать. Для транзакции 103 (создание смарт-контракта) вызываются методы, помеченные аннотацией ContractInit. Для транзакций 104 (вызов смарт-контракта на исполнение) — методы, помеченные аннотацией ContractAction. Подобный подход используется во многих фреймворках для мэппинга метода класса на route HTTP REST запроса. Блокчейн — key-value база данных, где каждый контракт имеет свое пространство имен, с которым он работает как с Redis или Hazelcast. SDK предоставляет для этого удобный и знакомый интерфейс. Для каждой транзакции (для вызова каждого метода) создается новый экземпляр ContractHandler, в который передается пустой виртуальный ContractState. Он отслеживает, для каких ключей были записаны значения текущим методом контракта, а также кеширует значения тех ключей, которые были непосредственно запрошены у ноды. Таким образом, мы имеем некий аналог незакомиченной транзакции БД: повторные чтения одного ключа вернут один и тот же результат; мы видим значения, которые только что записали; новые значения попадут на стейт контракта в ноду только в случае успеха (аналог коммита). С наборами однотипных объектов на стейте контракта можно работать через абстракцию Mapping. Внутри key/value хранилища Mapping аллоцирует пространство имен, на ключах которого хранятся объекты одного типа. Поддерживается получение коллекции таких объектов для переданного списка ключей. Перейдем к практике на нашем SDK. Напишем контракт, реализующий игру «камень-ножницы-бумага». Суть его следующая: при создании контракта передаем адреса игроков; каждый игрок генерирует случайную соль и отправляет хеш (ответ+соль) на контракт; как только все игроки сделали предыдущее действие, игра стартует; теперь каждый игрок должен раскрыть свой ответ, отправив значение соли на контракт; как только все игроки раскрыли свой ответ, вычисляется победитель (если все ответы одинаковые, то победителя нет). Полная версия кода есть на Github, здесь я приведу лишь самые важные фрагменты. Опишем необходимое взаимодействие с контрактом в его интерфейсе: Пометим методы контракта соответствующими аннотациями и перейдем непосредственно к реализации.  Создадим класс, пометив его аннотацией ContractHandler, и конструктор, в который наш framework передаст необходимые объекты — виртуальный state и текущую транзакцию. Опишем метод, обрабатывающий создание контракта (транзакция 103). Он будет осуществлять необходимые проверки и писать адреса игроков в соответствующий Mapping. Теперь реализуем метод, с помощью которого игроки смогут передать хеш от своего ответа: Для удобства выделим класс Game — он будет инкапсулировать часть логики контракта и хранить состояние. Как только все адреса прислали свои хеши, запускаем игру и кладем этот экземпляр Game на стейт. Теперь игроки должны прислать соль, которая была использована при хешировании. Для этого метод контракта play, проведя необходимые проверки, делегирует обработку классу Game в одноименный метод. В main-методе нашего контракта опишем настройку и запуск обработчика контракта из SDK: Остается собрать запускающий это docker-образ и опубликовать в registry, с которым работает нода Waves Enterprise. Для удобства можно использовать скрипт build_and_push_to_docker.sh из репозитория SDK. После того как контракт опубликован, отправляем транзакцию 103 для его создания. Здесь можно использовать REST API ноды.  Передаем список игроков с адресами 3M3xGmJGmxBv2aZ4UFmn93rHxVXTJDKSAnh и 3M7EEnszPAT2yr72SgWVDLxfYCa4AYvVRwv. Получаем ID транзакции — она будет идентификатором контракта. Проверим, что контракт успешно смайнился: Контракт с игрой создан для пользователей с адресами 3M7EEnszPAT2yr72SgWVDLxfYCa4AYvVRwv и 3M3xGmJGmxBv2aZ4UFmn93rHxVXTJDKSAnh. Сгенерируем соль для пользователя с адресом 3M7EEnszPAT2yr72SgWVDLxfYCa4AYvVRwv. Допустим, это будет значение "abcd1234". Возьмем SHA256-хеш от ответа + соли: Util.hash(AnswerType.SCISSORS.toString() + "_" + "abcd1234"). Отправим захешированный ответ для пользователя с адресом 3M7EEnszPAT2yr72SgWVDLxfYCa4AYvVRwv. Видим, что он появился на стейте контракта: Проделаем аналогичные действия для адреса 3M3xGmJGmxBv2aZ4UFmn93rHxVXTJDKSAnh. Его ответом будет «камень» (ROCK). Мы видим на стейте новый объект GAME: игра запустилась. Теперь игроки могут раскрыть значения своих солей. Например, так это сделает пользователь с адресом 3M7EEnszPAT2yr72SgWVDLxfYCa4AYvVRwv. После раскрытия всех игроков вычисляется победитель и игра переходит в статус FINISHED. На стейте появляется соответствующая информация, в том числе и адрес победителя — пользователя с адресом 3M3xGmJGmxBv2aZ4UFmn93rHxVXTJDKSAnh, показавшего камень. Данный пример можно доработать, добавив логику раундов. Так можно будет обрабатывать ситуацию, когда победителя нет, или поддержать игру для более чем двух игроков. В ближайшее время мы продолжим дорабатывать SDK. В приоритете — интеграция с нативными ассетами Waves Enterprise, которую мы добавим с соответствующим релизом ноды. Кроме того, мы планируем добавить клиент для вызова со стороны бэкенда, упростить работу с данными стейтов других контрактов, добавить модуль для удобного юнит-тестирования контрактов.