[recovery mode] Пошаговая инструкция как создать NFT коллекцию на Golang + Ethereum (часть 3)

Привет Хабр.
В прошлых статьях мы научились генерировать 10 000 изображений для нашей NFT коллекции с помощью Golang, а также загрузили все сгенерированные изображения в децентрализованное хранилище IPFS.
В этой статье мне хотелось бы поделиться знаниями и опытом, а также о подводных камнях, с которыми мне пришлось столкнуться при разработке смарт-контрактов для NFT коллекций на блокчейне Ethereum.
Мы создадим типовой смарт-контракт для нашей NFT коллекции, протестируем и загрузим созданный смарт-контракт в тестовую сеть Ethereum. Но прежде, чем мы приступим к кодингу, мне хотелось бы остановиться на ERC-721 стандарте, данный стандарт описывает спецификацию NFT токенов.
Давайте подробней рассмотрим, какие методы должны быть у нашего смарт-контракта:
function balanceOf(address _owner) external view returns (uint256);
function ownerOf(uint256 _tokenId) external view returns (address);
function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable;
function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable;
function transferFrom(address _from, address _to, uint256 _tokenId) external payable;
function approve(address _approved, uint256 _tokenId) external payable;
function setApprovalForAll(address _operator, bool _approved) external;
function getApproved(uint256 _tokenId) external view returns (address);
function isApprovedForAll(address _owner, address _operator) external view returns (bool);
Полную спецификацию по ERC-721 стандарту можно посмотреть тут.
Нам не обязательно реализовывать весь стандарт самим, более оптимальным подходом является переиспользовать готовые библиотеки: OpenZeppelin — это библиотека для разработки безопасных смарт-контрактов и именно с ней мы будем работать.
Плюсы такого решения очевидны:
Готовый код из под коробки, прошедший аудит безопасности
Аудит нашего смарт-контракта займет гораздо меньше денег и времени
Создание смарт-контракта
Давайте создадим типовой смарт-контракт для нашей NFT коллекции, назовем её MonkeyNFT. Наш смарт-контракт наследует стандартные OpenZeppelin библиотеки, а именно:
ERC721 / ERC721Enumerable — контрактные модули, которые обеспечивают базовые функциии для нашего NFT токена
Ownable — контрактный модуль, который обеспечивает базовый механизм контроля доступа
Разрабатывать смарт-контракт, мы будем с помощью такого инструмента, как hardhat, очень крутой инструмент для разработки, тестирования и деплоя, особенно для тех, кто устал от Truffle и его бесконечного количества багов.
Запускаем команду npx hardhat init, для создания нового шаблона для нашего смарт-контракта, далее переходим в директорию contracts и пишем код:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/utils/Context.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract MonkeyNFT is ERC721, ERC721Enumerable, Ownable {
using SafeMath for uint256;
uint public constant maxPurchase = 10;
uint256 public constant MAX_MONKEYS = 10000;
uint256 private _monkeyPrice = 80000000000000000; //0.08 ETH
string private baseURI;
bool public saleIsActive = true;
constructor() ERC721("The Monkey NFT", "MNK") {
}
function _beforeTokenTransfer(address from, address to, uint256 tokenId)
internal
override(ERC721, ERC721Enumerable)
{
super._beforeTokenTransfer(from, to, tokenId);
}
function supportsInterface(bytes4 interfaceId)
public
view
override(ERC721, ERC721Enumerable)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
function withdraw() public onlyOwner {
uint256 balance = address(this).balance;
payable(msg.sender).transfer(balance);
}
function setPrice(uint256 _newPrice) public onlyOwner() {
_monkeyPrice = _newPrice;
}
function getPrice() public view returns (uint256){
return _monkeyPrice;
}
function mintMonkeys(uint numberOfTokens) public payable {
require(saleIsActive, "Sale must be active to mint Monkeys");
require(numberOfTokens <= maxPurchase, "Can only mint 10 tokens at a time");
require(totalSupply().add(numberOfTokens) <= MAX_MONKEYS, "Purchase would exceed max supply of Monkeys");
require(_monkeyPrice.mul(numberOfTokens) <= msg.value, "Ether value sent is not correct");
for(uint i = 0; i < numberOfTokens; i++) {
uint mintIndex = totalSupply();
if (totalSupply() < MAX_MONKEYS) {
_safeMint(msg.sender, mintIndex);
}
}
}
function _baseURI() internal view override returns (string memory) {
return baseURI;
}
function setBaseURI(string memory newBaseURI) public onlyOwner {
baseURI = newBaseURI;
}
function flipSaleState() public onlyOwner {
saleIsActive = !saleIsActive;
}
}
Из интересного:
Флаг
saleIsActive— говорит, о том, что наша коллекция либо готова к продаже, либо нет. Данная функция очень полезна на начальном этапе, когда по каким то причинам необходимо остановить продажи.Переменная
maxPurchaseхранит кол-во токенов, который пользователь может купить за один раз. Некоторая защита от ботов, чтобы не выкупили всю коллекцию сразу.Функция
mintMonkey— это основная функция, через которую пользователи могут купить наш NFT токен. Модификаторpayable, как раз говорит о том, что вызов данной функции для пользователя платный.
Для того, чтобы скомпилировать наш смарт-контракт, запускаем команду npx hardhat compile, после чего создадутся abi-артифакты в директории artifacts.
Тестирование смарт-контракта
Т.к. загруженный смарт-контракт нельзя модифицировать, нам обязательно необходимо его протестировать до деплоя в блокчейн Ethereum. Переходим в директорию test, удаляем дефолтные тесты и пишем свои:
const { expect } = require("chai");
describe("Token contract", () => {
let contract;
let owner;
let addr1;
let addr2;
let addrs;
let baseURI;
beforeEach(async () => {
const Token = await ethers.getContractFactory("MonkeyNFT");
[owner, addr1, addr2, ...addrs] = await ethers.getSigners();
contract = await Token.deploy();
baseURI = "https://hardhat.org/test/"
await contract.setBaseURI(baseURI)
});
it("Should initialize contract", async () => {
expect(await contract.MAX_MONKEYS()).to.equal(10000);
});
it("Should set the right owner", async () => {
expect(await contract.owner()).to.equal(await owner.address);
});
it("Should mint", async () => {
const price = await contract.getPrice();
const tokenId = await contract.totalSupply();
expect(
await contract.mintMonkeys(1, {
value: price,
})
).to.emit(contract, "Transfer").withArgs(ethers.constants.AddressZero, owner.address, tokenId);
expect(await contract.tokenURI(tokenId)).to.equal(baseURI+"0");
});
});
Запускаем тестирование смарт-контракта, командой npx hardhat test. Если все тесты пройдены, то мы увидем:
Compiling 1 file with 0.8.4
Compilation finished successfully
Token contract
✓ Should initialize contract
✓ Should set the right owner
✓ Should mint (41ms)
3 passing (1s)
Только, что мы успешно протестировали:
Создание смарт-контракта
Успешно установили владельца смарт-контракта
Успешно вызвали платную функцию
mintMonkeysдля продажи NFT токена
Деплой смарт-контракта
Деплоить мы будем в тестовую сеть Ethereum. Переходим в директорию scripts и удаляем дефолтные скрипты, они нам больше не понадобяться и пишем новый скрипт деплоя:
async function main() {
const [deployer] = await ethers.getSigners();
console.log("Deploying contracts with the account:", deployer.address);
console.log("Account balance:", (await deployer.getBalance()).toString());
const Token = await ethers.getContractFactory("MonkeyNFT");
console.log("Deploying contract...");
const token = await Token.deploy();
await token.deployed();
console.log("Token address:", token.address);
}
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});
Так же нам необходимо изменить файл hardhat.config следующим образом:
require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
module.exports = {
solidity: "0.8.4",
networks: {
rinkeby: {
url: `https://eth-rinkeby.alchemyapi.io/v2/${YOUR_API_KEY}`,
},
},
};
Отлично все готово для деплоя, запускаем скрипт: npx hardhat run scripts/deploy.js --network rinkeby
В следующей статье я расскажу как взаимодействовать с нашим созданным смарт-контрактом с помощью web3.js.
