За последние несколько лет нефункциональные токены (НФТ) стали источником множества новостей и спекуляций. Наиболее известным примером их использования является продажа цифровых художественных активов, которые обычно используются в качестве фотографий профиля. Наиболее известные примеры – CryptoPunks и Bored Apes.
Хотя многие НФТ представляют собой коллекции заранее сгенерированных произведений искусства, некоторые из них включают в себя динамический генеративный аспект, при котором искусство создается на лету по мере чеканки отдельных токенов. Это вполне осуществимо для НФТ с изображением профиля, которые часто состоят из одного базового изображения в различных цветах, с различными функциями и аксессуарами. В некоторых случаях, таких как CryptoKitties, новые дочерние токены могут быть созданы путем “разведения” существующих токенов, что придает дочернему токену характеристики обоих родителей.
В этом руководстве, состоящем из двух частей, мы создадим простой генеративный художественный НФТ, ReplBots. Первая часть урока будет посвящена смарт-контракту NFT в Ethereum, а вторая – созданию веб-фронтенда, или dApp. В этом учебнике вы:
- Узнаете, как создать NFT для генеративного искусства на блокчейне Ethereum.
- Изучите важные концепции разработки смарт-контрактов, такие как случайность.
- Узнаете, как взаимодействовать с блокчейном из JavaScript-кода фронтенда.
Начало работы
Для начала работы над проектом нам понадобятся две вещи: Solidity repl и браузерный кошелек.
Solidity repl
Войдите в Replit или создайте учетную запись, если вы еще этого не сделали. После входа в систему создайте Solidity starter repl.
Стартовая реплика Solidity работает немного иначе, чем другие реплики, которые вы, возможно, использовали в прошлом. Вместо того чтобы запускать нашу реплику каждый раз, когда мы хотим протестировать новую часть кода, мы можем запустить нашу реплику один раз, чтобы запустить ее, и она будет автоматически перезагружаться при внесении изменений в наш код Solidity в contract.sol
.
Solidity starter repl поставляется с дружественным веб-интерфейсом, построенным с использованием web3 Ethereum JavaScript API, который мы будем использовать для развертывания и взаимодействия с нашими контрактами. Мы будем развертывать на Replit Testnet, пользовательской версии блокчейна Ethereum, управляемой Replit и оптимизированной для тестирования.
Браузерный кошелек
Нам понадобится браузерный web3-кошелек для взаимодействия с Replit Testnet и нашими развернутыми контрактами. MetaMask – это популярный и многофункциональный кошелек, реализованный как WebExtension. Вы можете установить его со страницы загрузки MetaMask. Убедитесь, что вы используете поддерживаемый браузер – Chrome, Firefox, Brave или Edge.
После установки MetaMask следуйте подсказкам, чтобы создать кошелек и войти в систему. MetaMask выдаст вам секретную фразу восстановления из 12 слов – это приватный ключ вашего кошелька, который необходимо хранить в тайне. Если вы потеряете эту фразу, вы не сможете получить доступ к своему кошельку. Если кто-то другой найдет ее, то сможет.
Если вы уже используете MetaMask, мы рекомендуем создать новый аккаунт для тестирования Replit. Вы можете сделать это из меню аккаунта, которое появляется при нажатии на аватар аккаунта в правом верхнем углу интерфейса MetaMask.
Введение в разработку смарт-контрактов
Не стесняйтесь пропустить этот раздел, если вы уже писали контракты на Solidity или прошли наш учебник по эскроу.
Теперь, когда у нас настроена наша реплика и кошелек, мы можем приступить к разработке. Мы будем писать контракты, которые являются основными строительными блоками программ Ethereum. Отдельный контракт может иметь переменные состояния и функции и может наследоваться от множества других контрактов. Контракты в Solidity эквивалентны классам в таких языках, как Python или Java.
Контракты в Ethereum и сетях на основе Ethereum (таких как Replit Testnet и Binance Chain) компилируются в байткод, который запускается на виртуальной машине Ethereum (EVM). Это похоже на то, как код Java компилируется в байткод для JVM.
Список опкодов для EVM представлен здесь. Многие опкоды, связанные с арифметическими и логическими операциями, должны быть вам знакомы, если вы раньше работали с каким-либо видом ассемблера. EVM также имеет опкоды для операций, специфичных для блокчейна, таких как получение информации о текущем блоке или цепи.
Дополнительным моментом, уникальным для разработки блокчейна, является то, что каждый опкод имеет свою цену (указана здесь). Пользователи смарт-контрактов платят плату (известную как газ) за вызов функций, изменяющих состояние. Эта плата определяется используемыми опкодами, поэтому разработчики заинтересованы в том, чтобы их код был как можно проще.
Solidity, который мы будем использовать ниже, является самым популярным языком для разработки смарт-контрактов на Ethereum. Существуют альтернативные варианты, такие как Vyper, но они не так широко используются.
Дизайн NFT
Наш ReplBot NFT будет состоять из базового робота и трех аксессуаров. Базовый робот выглядит следующим образом:
Каждый NFT будет иметь три разных цвета для рамки, козырька и фона. У каждого NFT также будет три различных аксессуара: головной убор (шляпа или парик), ушной убор (уши животных или наушники) и лицевой убор (очки или маски). Некоторые примеры:
Какие цвета и аксессуары будут у каждого NFT, определяется случайным образом во время создания.
Эшафот кода контракта
Давайте начнем работу над кодом контракта NFT. Откройте contract.sol
, удалите содержимое файла и добавьте следующий скелет:
// // SPDX-License-Identifier: MIT
pragma solidity ^0.8.8;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/utils/Strings.sol";
contract ReplBots is ERC721Enumerable {
uint256 public tokenCounter;
constructor() ERC721("ReplBots", "RBNFT") {
}
function mint(address recipient) public returns (uint256) {
uint tokenId = tokenCounter;
tokenCounter++;
_safeMint(recipient, tokenId);
return tokenId;
}
}
Первая строка нашего контракта – это идентификатор лицензии. Смарт-контракты обычно с открытым исходным кодом, и повторное использование кода других проектов является обычным явлением в DeFi, поэтому хорошей идеей будет включить лицензию, которая указывает, как вы хотите, чтобы другие использовали (или не использовали) ваш код. Список поддерживаемых лицензий приведен здесь.
В следующей строке мы определяем версию Solidity, которую мы используем (в данном случае это любая версия новее 0.8.13 до 0.9.0). Solidity – развивающийся язык, и в него часто вносятся изменения, поэтому наш код может не компилироваться под более старыми или новыми версиями языка.
После оператора pragma мы импортируем некоторые контракты из библиотеки контрактов OpenZeppelin. Эта библиотека включает наследуемые базовые контракты для широкого спектра общих потребностей разработки смарт-контрактов, включая базовые реализации стандартов токенов Ethereum.
Стандарт токенов Ethereum для NFT – ERC-721. Вместо того чтобы самостоятельно реализовывать все функциональные возможности этой спецификации, мы можем импортировать и использовать версию OpenZeppelin. Обратите внимание, что хотя OpenZeppelin имеет базовый контракт ERC721
, мы будем использовать расширение/дочерний контракт ERC721Enumerable
, который, в дополнение ко всем стандартным функциям ERC-721, позволит нам перечислить, какие NFT принадлежат тем или иным владельцам. Эта функциональность жизненно важна для нашего фронт-энда.
Мы также используем библиотеку OpenZeppelin Strings
, которая предоставляет несколько полезных функций для работы со строками.
Наше определение контракта указывает, что мы будем наследовать от ERC721Enumerable
:
contract ReplBots is ERC721Enumerable {
В теле контракта мы определяем переменную состояния tokenCounter
. Это обеспечит нас уникальными, увеличивающимися идентификаторами для токенов по мере их чеканки. Solidity автоматически инициализирует все переменные в 0, поэтому нам не нужно делать это явно.
Обратите внимание на использование uint256
в качестве типа для этих значений: Поскольку большинство значений, с которыми мы имеем дело в программах Solidity, являются денежными, мы используем беззнаковые целые числа, чтобы избежать хранения отрицательных чисел. В версиях Solidity до 0.8.0 это иногда приводило к опасным переполнениям, но теперь защита от переполнения встроена в язык.
Далее у нас есть метод constructor()
, который вызывает родительский конструктор для определения имени и символа/биржевого тикера нашего NFT.
constructor() ERC721("ReplBots", "RBNFT") {
}
Затем мы определяем функцию mint
, чтобы мы могли создавать НМТ.
function mint(address recipient) public returns (uint256) {
uint tokenId = tokenCounter;
tokenCounter++;
_safeMint(recipient, tokenId);
return tokenId;
}
Пока все, что она делает, это увеличивает tokenCounter
, создает новый токен, используя ERC721._safeMint
, и возвращает числовой идентификатор майнингового NFT. Использование _safeMint
вместо _mint
не позволит нам чеканить токены получателю, у которого нет реализованного метода для их получения. Это не позволит нашим токенам застрять в адресах контрактов и стать непригодными для использования.
Функция mint()
является public
функцией, что означает, что ее могут вызывать как внешние пользователи, так и другие функции в этом контракте или любых контрактах, которые наследуются от него. Solidity предоставляет гранулированные опции видимости функций и переменных, которые объясняются здесь.
Структуры данных NFT
Давайте заполним скелет нашего кода, начиная со структур данных, которые понадобятся нам для хранения информации о каждом NFT, который мы создаем. Согласно нашему проекту, каждый NFT будет иметь три аксессуара и три цвета. Мы можем представить аксессуары в виде строк, а цвета – в виде значений RGB.
Мы начнем с определения массива строк для каждого типа аксессуаров. Добавьте следующий код над определением constructor
:
string[] private headgear = [
"Cowboy Hat",
"Fro",
"Baseball Cap",
"Viking Helmet"
];
string[] private eargear = [
"Bunny Ears",
"Headphones"
];
string[] private facegear = [
"Sunglasses",
"Moustache",
"Nose",
"DOOM Mask"
];
Не стесняйтесь рисовать и добавлять другие аксессуары в эти списки.
Далее мы создадим структуру для определения цветов. Добавьте этот код под определениями ваших массивов:
struct Color {
uint8 red;
uint8 green;
uint8 blue;
}
Каждый цвет имеет красный, зеленый и синий компоненты, представленные в виде 8-битного беззнакового целого числа. Большинство беззнаковых целых чисел, с которыми мы работаем в контрактах, являются 256-битными, настолько, что Solidity предоставляет псевдоним uint
для uint256
. Тем не менее, хорошей практикой является использование меньших значений, где это возможно, из-за затрат на газ – это известно как плотная упаковка переменных. 8-битное беззнаковое целое число может содержать значение от 0 до 255, что является именно тем диапазоном, который нам нужен для хранения стандартных значений RGB.
Далее мы создадим struct для самого ReplBot, состоящий из наших трех цветов и трех аксессуаров. Добавьте этот код под предыдущим определением struct:
struct ReplBot {
Color frame;
Color visor;
Color background;
uint8 head;
uint8 ears;
uint8 face;
}
Для экономии места мы будем хранить значения аксессуаров как индексы в массивах, которые мы определили выше. uint8
позволяет нам определить максимум 255 аксессуаров для каждого типа, что более чем достаточно.
Наконец, мы определим тип отображения, который позволит нам связать числовые идентификаторы токенов со структурами ReplBot. Добавьте эту строку под последним определением структуры:
mapping (uint => ReplBot) private replbots;
Майнинг NFTs
Теперь, когда у нас есть наши структуры данных, мы можем вернуться к нашей функции mint
и расширить ее. Замените тело функции mint()
следующим кодом:
function mint(address recipient) public returns (uint256) {
// Get ID and increment counter
uint tokenId = tokenCounter;
tokenCounter++;
// Determine colors
Color memory frameCol = Color(
uint8(_random(tokenId, "QWERT") % 255),
uint8(_random(tokenId, "YUIOP") % 255),
uint8(_random(tokenId, "ASDFG") % 255));
Color memory visorCol = Color(
uint8(_random(tokenId, "HJKL;") % 255),
uint8(_random(tokenId, "ZXCVB") % 255),
uint8(_random(tokenId, "BNM,.") % 255));
Color memory backgroundCol = Color(
uint8(_random(tokenId, "12345") % 255),
uint8(_random(tokenId, "67890") % 255),
uint8(_random(tokenId, "[]{}'") % 255));
// Determine accessories
uint8 headIdx = uint8(_random(tokenId, "qwert") % headgear.length);
uint8 earIdx = uint8(_random(tokenId, "yuiop") % eargear.length);
uint8 faceIdx = uint8(_random(tokenId, "asdfg") % facegear.length);
// Create bot
replbots[tokenId] = ReplBot(frameCol, visorCol, backgroundCol, headIdx, earIdx, faceIdx);
// Mint token
_safeMint(recipient, tokenId);
return tokenId;
}
Основная часть этого кода состоит из получения случайных чисел и использования оператора modulo для преобразования их в число в нужном нам диапазоне – для цветов это будет число от 0 до 255, а для аксессуаров это будет действительный индекс в соответствующем массиве аксессуаров.
Когда мы определяем наши структуры Color
, мы указываем местоположение данных memory
. Расположение данных должно быть указано для всех сложных типов данных: массивов, структур и строк. Для переменных, локальных для функции, обычно правильно memory
.
После этого мы создаем структуру ReplBot, присваиваем ей ID нашего токена с помощью связки, а затем майним токен.
Однако, если вы попытаетесь скомпилировать этот код, вы заметите, что _random()
не определен. В отличие от многих традиционных языков, Solidity не имеет собственного способа генерации псевдослучайных чисел. Поэтому нам придется определить его самостоятельно.
Случайность в контрактах Ethereum – это сложная вещь, которую сложно правильно реализовать, и которая может быть опасной, если сделать это неправильно, в зависимости от того, для чего она используется. Если вы изучали случайность в других языках, то знаете, что случайные числа, которые мы используем в программировании, обычно являются лишь псевдослучайными, полученными из заранее определенного семени или основанными на времени выполнения. Это нормально, когда мы генерируем мир Minecraft, но создает проблемы, скажем, для лотереи на основе блокчейна. Поскольку Ethereum является прозрачной и распределенной сетью, у нас нет возможности скрыть семя, а такие значения, как текущее время, могут манипулироваться майнерами, стремящимися получить прибыль.
Единственный надежный способ генерировать случайные числа на данный момент – использовать надежный внешний источник случайности, например, VRF-оракул Chainlink (оракул – это поток данных, созданный для использования смарт-контрактами). Однако, поскольку мы строим контракт на базе Replit Testnet, у нас не будет доступа к такому оракулу, поэтому нам придется довольствоваться лучшей псевдослучайностью, которую мы можем получить. Если бы мы создавали лотерейный контракт, это было бы проблемой, но для данного проекта манипуляции могут дать вам только немного отличающийся по внешнему виду ReplBot.
Наша функция _random()
должна быть вставлена ниже функции mint()
. Она выглядит следующим образом:
function _random(uint tokenId, string memory input) internal view returns (uint256) {
bytes32 blckhash = blockhash(block.number - 1);
return uint256(keccak256(abi.encodePacked(block.difficulty, blckhash, tokenId, abi.encodePacked(input))));
}
Это функция view
, потому что она не изменяет состояние. В первой строке мы получаем блокчейн последнего добытого блока. Во второй строке мы используем abi.encodePacked()
для конкатенации:
- Блокчейн, который мы извлекли ранее.
- Сложность текущего блока.
- ID токена, который мы майним.
- Строка
input
, которую мы передали при вызове_random()
.
Все эти значения, кроме последнего, будут одинаковыми для каждого вызова _random()
в отдельном вызове mint()
. Вот почему мы указали разные строки для каждого из них.
Затем мы хэшируем наш большой кусок данных с помощью keccak256()
. Хорошая хэш-функция будет возвращать очень разные результаты при одинаковых входных данных, поэтому это гарантирует, что каждый вызов _random()
будет возвращать достаточно разные результаты, даже если большинство входных данных одинаковы.
Наконец, мы преобразуем хэш в целое число без знака, которое будет возвращено в качестве окончательного “случайного” числа.
Просмотр данных токена
Теперь, когда мы можем генерировать токены с интересно выглядящими ReplBots, нам нужен способ получения информации о них, чтобы мы могли отобразить их на фронтенде web3, который мы создадим во второй части этого руководства. Для этого мы напишем две функции: botAccessories
и botColors
. Это будут вызываемые извне функции представления, которые будут принимать идентификатор токена и возвращать три строки, подробно описывающие аксессуары и цвета, соответственно. Поскольку внешние вызовы функций представления бесплатны, нам не нужно слишком беспокоиться о затратах на газ в этих функциях.
Введите следующий код между определениями mint()
и _random()
:
function botAccessories(uint256 tokenId) public view returns (string memory, string memory, string memory) {
require(_exists(tokenId), "ReplBots: Query for nonexistent token");
ReplBot memory bot = replbots[tokenId];
return (headgear[bot.head], eargear[bot.ears], facegear[bot.face]);
}
Тело нашей функции начинается с оператора require. Это функция обработки ошибок в Solidity: если условие в первом аргументе не выполняется, текущая транзакция возвращается (отменяя все предыдущие действия) и выводится сообщение об ошибке во втором аргументе. В данном случае мы используем эту функцию для предотвращения запроса информации о немитированных токенах.
Убедившись, что tokenId
действителен, мы получаем связанную с ним структуру ReplBot
. Solidity позволяет нам возвращать несколько значений из функции, что мы и используем для возврата трех разных строк.
Теперь давайте создадим botColors()
. Добавьте следующий код ниже определения botAccessories()
:
function botColors(uint256 tokenId) public view returns (string memory, string memory, string memory) {
require(_exists(tokenId), "ReplBots: Query for nonexistent token");
ReplBot memory bot = replbots[tokenId];
return (_colorToString(bot.frame),
_colorToString(bot.visor),
_colorToString(bot.background));
}
Эта функция очень похожа на botAccessories()
, но так как наши цвета являются структурами, а не строками, нам потребуется определить новую функцию _colorToString()
для их преобразования. Давайте сделаем это сейчас.
Добавьте следующий код ниже определения botColors()
:
function _colorToString(Color memory color) internal pure returns (string memory) {
string[7] memory parts;
parts = ["(",
color.red.toString(),
",",
color.blue.toString(),
",",
color.green.toString(),
")"];
return string(abi.encodePacked(parts[0], parts[1], parts[2], parts[3], parts[4], parts[5], parts[6]));
}
Хотя Solidity в настоящее время не имеет функции объединения строк, мы можем использовать abi.encodePacked
на массиве строк для достижения того же результата, аналогично тому, что мы делали в _random()
. В Solidity также нет синтаксического сахара для расширения/развертывания массивов в аргументы функций, поэтому нам придется указывать каждый элемент отдельно.
Но подождите! Откуда взялся .toString()
? Если вы попытаетесь скомпилировать этот контракт сейчас, вы обнаружите, что этот метод не определен.
Вместо того, чтобы быть встроенным в Solidity, он берется из библиотеки OpenZeppelin Strings, которую мы импортировали в начале этого руководства. Чтобы использовать эту библиотеку, нам нужно добавить следующую строку в определение нашего контракта, прямо под contract ReplBots is ERC721Enumerable {
:
using Strings for uint8;
Директива Solidity using Library for type
прикрепляет все функции из указанной библиотеки к указанному типу. Когда библиотечная функция вызывается на объекте этого типа, она будет получать этот объект в качестве первого параметра.
Тестирование нашего контракта
Теперь, когда мы реализовали основную функциональность нашего NFT, пришло время развернуть его в Replit Testnet и протестировать. Для этого мы будем использовать веб-интерфейс нашей программы repl.
Во-первых, запустите ваш repl. После установки всех зависимостей вы должны увидеть веб-интерфейс Replit Ethereum в браузере вашего repl. Он выглядит следующим образом:
Подключите свой кошелек MetaMask к веб-интерфейсу и переключитесь на Replit Testnet. Затем нажмите на ссылку, чтобы получить 1 ETH для тестирования. Подождите, пока 1 ETH не появится в балансе вашего кошелька в правом верхнем углу страницы.
Теперь вы можете развернуть свои контракты. Выберите “ReplBots” из выпадающего списка и нажмите “Deploy”. Одобрите появившееся всплывающее окно MetaMask.
Когда контракт будет развернут, он отобразится в виде расширяемого поля под выпадающим окном. Разверните его и посмотрите на все доступные функции.
Заработайте свой первый NFT, перейдя к функции mint
. Нажмите на адрес своего кошелька в правом верхнем углу страницы, чтобы скопировать его, а затем вставьте его в поле recipient
. Затем запустите функцию и одобрите появившееся всплывающее окно MetaMask.
Через несколько секунд вы должны увидеть всплывающее окно, указывающее на то, что ваша транзакция прошла. Поздравляем, вы стали гордым владельцем ReplBot NFT! Проверьте его цвета и аксессуары, введя ID 0 в botColors
и botAccessories
.
Если вы повторите чеканку, вы получите ReplBot с ID 1 и другим набором цветов и аксессуаров.
Разведение ботов
Теперь мы можем чеканить ReplBot’ов со случайными характеристиками, что соответствует спецификации, которую мы изложили в начале этого руководства. Но с помощью небольшого дополнительного кода мы можем ввести второй способ создания ReplBots – разведение.
Для нашей функции breed
мы примем два отдельных ReplBots, принадлежащих вызывающему, и создадим нового со следующими характеристиками:
- Его цвета будут в равной степени сочетать цвета обоих родителей.
- Его головной убор будет происходить от первого родителя.
- Ушная раковина будет получена от второго родителя.
- Его лицевая одежда будет рандомизирована.
Чтобы отслеживать, какие боты добываются, а какие размножаются, мы добавим несколько дополнительных полей в структуру ReplBot
. Перейдите в верхнюю часть вашего контракта и отредактируйте структуру так, чтобы она выглядела следующим образом:
struct ReplBot {
Color frame;
Color visor;
Color background;
uint8 head;
uint8 ears;
uint8 face;
uint256 generation; // new field
uint256 parentOneId; // new field
uint256 parentTwoId; // new field
}
Мы будем хранить поколение нашего бота и идентификаторы обоих родителей. Боты, созданные через mint
, будут иметь 0 для всех этих трех значений. Чтобы не выглядело так, будто все боты, созданные с помощью mint, имеют токены 0 для обоих родителей, мы начнем счетчик токенов с 1. Отредактируйте его объявление (в верхней части вашего контракта) следующим образом:
uint256 public tokenCounter = 1; // no more token 0
Затем нам нужно добавить три нуля к созданию структуры ReplBot
в mint()
. Найдите и отредактируйте строку ниже:
// Create bot
replbots[tokenId] = ReplBot(frameCol, visorCol, backgroundCol, headIdx, earIdx, faceIdx, 0, 0, 0); // <-- ZEROS ADDED
// Mint token
_safeMint(recipient, tokenId);
return tokenId;
Теперь мы можем приступить к нашей функции breed()
. Добавьте следующий код ниже определения mint()
:
function breed(uint256 parentOneId, uint256 parentTwoId, address recipient) public returns (uint256) {
// Require two parents
require(parentOneId != parentTwoId, "ReplBots: Parents must be separate bots");
// Check ownership
require(ownerOf(parentOneId) == msg.sender, "ReplBots: You don't own parent 1");
require(ownerOf(parentTwoId) == msg.sender, "ReplBots: You don't own parent 2");
ReplBot storage parentOne = replbots[parentOneId];
ReplBot storage parentTwo = replbots[parentTwoId];
// Check age
require(parentOne.generation == parentTwo.generation, "ReplBots: Parents must belong to the same generation");
}
Мы начинаем нашу функцию с некоторых проверок: Два родительских ID должны быть разными, они должны принадлежать вызывающей функции (msg.sender
), и они должны быть в одном поколении.
Далее следует код создания бота, который будет похож на код в нашей функции mint()
. Добавьте следующий код под последним утверждением require()
в функции breed
, описанной выше:
// Increment token counter
uint tokenId = tokenCounter;
tokenCounter++;
// Interpolate colors
Color memory frameCol = Color(_meanOfTwo(parentOne.frame.red, parentTwo.frame.red),
_meanOfTwo(parentOne.frame.green, parentTwo.frame.green),
_meanOfTwo(parentOne.frame.blue, parentTwo.frame.blue));
Color memory visorCol = Color(_meanOfTwo(parentOne.visor.red, parentTwo.visor.red),
_meanOfTwo(parentOne.visor.green, parentTwo.visor.green),
_meanOfTwo(parentOne.visor.blue, parentTwo.visor.blue));
Color memory backgroundCol = Color(_meanOfTwo(parentOne.background.red, parentTwo.background.red),
_meanOfTwo(parentOne.background.green, parentTwo.background.green),
_meanOfTwo(parentOne.background.blue, parentTwo.background.blue));
// Choose accessories
uint8 headIdx = parentOne.head;
uint8 earIdx = parentTwo.ears;
uint8 faceIdx = uint8(_random(tokenId, "asdfg") % facegear.length);
// Create bot
replbots[tokenId] = ReplBot(frameCol, visorCol, backgroundCol, headIdx, earIdx, faceIdx, parentOne.generation + 1, parentOneId, parentTwoId);
// Mint token
_safeMint(recipient, tokenId);
return tokenId;
Все цвета и атрибуты нашего бота генерируются в соответствии с процессом разведения, который мы указали выше. Последнее, что нам нужно сделать, это определить функцию _meanOfTwo()
, которую мы используем для определения цветов дочернего бота. Добавьте следующий код в нижнюю часть вашего контракта, чуть ниже определения _random()
:
function _meanOfTwo(uint8 first, uint8 second) internal pure returns (uint8) {
return uint8((uint16(first) + uint16(second))/2);
}
Здесь мы преобразуем первое и второе в значения uint16
, чтобы предотвратить их переполнение (что привело бы к откату нашей функции), делим результат на два и возвращаем его в виде uint8
.
Наконец, нам нужно определить новую функцию view
, которая будет возвращать поколение и родство отдельных токенов. Вставьте определение следующей функции, botParentage()
, чуть ниже определения botColors()
:
function botParentage(uint256 tokenId) public view returns (uint, uint, uint) {
require(_exists(tokenId), "ReplBots: Query for nonexistent token");
ReplBot memory bot = replbots[tokenId];
return (bot.generation, bot.parentOneId, bot.parentTwoId);
}
Скомпилируйте и разверните ваш контракт, как вы делали раньше. Поскольку это новый контракт, вам нужно будет создать двух новых ReplBots, прежде чем вы сможете опробовать функцию breed
. Сделайте это сейчас и проверьте, что ваш дочерний бот (ID 2) выглядит так, как ожидалось. Затем вызовите botParentage
, чтобы проверить его поколение и родителей.
Добавление событий
Наш контракт теперь полностью функционален, но есть несколько тонкостей, которые мы можем добавить, в виде событий. События обеспечивают удобную для пользователя форму регистрации на блокчейне и широко используются dApps. Считается лучшей практикой испускать события при каждом изменении состояния, поэтому мы должны определить события для двух действий нашего контракта, изменяющих состояние, – майнинга и селекции.
Определения событий обычно размещаются в нижней части контрактов. Добавьте следующий код прямо над последней закрывающей скобкой контракта (}
):
event ReplBotCreated(address recipient, uint tokenId);
event ReplBotBorn(address recipient, uint tokenId, uint parentOneId, uint parentTwoId, uint generation);
Затем вставьте выброс события в конец функции mint()
, как показано ниже:
_safeMint(recipient, tokenId);
emit ReplBotCreated(recipient, tokenId); // <-- NEW LINE
return tokenId;
И еще одно в конце функции breed()
:
_safeMint(recipient, tokenId);
emit ReplBotBorn(recipient, tokenId, parentOneId, parentTwoId, parentOne.generation + 1); // <-- NEW LINE
return tokenId;
Следующие шаги
Мы закончили с кодом нашего контракта. Во второй части этого руководства мы создадим фронтенд dApp для пользователей, чтобы они могли майнить, просматривать и разводить ReplBot NFT. Если вы хотите сначала потратить немного больше времени на изучение Solidity, вот некоторые способы, которыми вы, возможно, захотите изменить и расширить этот контракт:
- Добавить больше аксессуаров. Вы должны будете нарисовать их сами!
- Рефакторить код, чтобы уменьшить размер контракта и сделать отдельные функции более газоэффективными. Один из быстрых способов сделать это – изменить видимость функций
public
наexternal
. - Изменить алгоритм размножения.
- Введем механизм размножения, который позволит пользователям разводить ReplBots, которые им не принадлежат.
Вы можете найти наш repl здесь.