Ленивая чеканка NFT — Solidity, Hardhat


Что такое NFT?

Нефунгицидные токены, часто называемые NFT, — это токены на основе блокчейна, каждый из которых представляет собой уникальный актив, например, произведение искусства, цифровой контент или медиа.

Почему ленивый майнинг?

Прежде чем понять, что такое ленивая майнинг, давайте разберемся, зачем она нам нужна. При майнинге NFT владельцу нужно заплатить Gas fee, это плата, которую создатель или тот, кто создал NFT, должен заплатить в обмен на вычислительную энергию, необходимую для обработки и подтверждения транзакций на блокчейне. Таким образом, при ленивой майнинг нам не нужно платить gas fee при листинге NFT, мы платим gas fee только когда мы действительно майним NFT, как только актив куплен и передан по цепи.

Как это работает

Обычно, когда мы майним NFT, мы вызываем функцию контракта напрямую & mint NFT on Chain. Но в случае ленивого майнинга, создатель готовит криптографическую подпись с помощью закрытого ключа своего кошелька.
Эта криптографическая подпись известна как «Ваучер», который затем используется для погашения NFT. Она также может включать некоторую дополнительную информацию, необходимую при майнинге NFT на Chain.

Технологический стек

Solidity & Hardhat для разработки смарт-контрактов
React Js & Tailwind CSS для разработки dapp

Давайте начнем

С создания & понимания подписанного ваучера.Для того, чтобы выполнить подписание, мы будем использовать
EIP-712: Типизированное хеширование и подписание структурированных данных
Это позволяет нам стандартизировать подписание типизированной структуры данных, которая затем может быть отправлена в смарт-контракт для получения NFT.

например.

struct SomeVoucher {
  uint256 tokenId;
  uint256 someVariable;
  uint256 nftPrice;
  string uri;
  bytes signature;
}
Войти в полноэкранный режим Выход из полноэкранного режима

Создайте новый каталог lazymint.

> mkdir lazymint
> cd lazymint 
> yarn add -D hardhat

Войдите в полноэкранный режим Выйти из полноэкранного режима

Далее, инициализируйте среду разработки hardhat

> yarn hardhat

888    888                      888 888               888
888    888                      888 888               888
888    888                      888 888               888
8888888888  8888b.  888d888 .d88888 88888b.   8888b.  888888
888    888     "88b 888P"  d88" 888 888 "88b     "88b 888
888    888 .d888888 888    888  888 888  888 .d888888 888
888    888 888  888 888    Y88b 888 888  888 888  888 Y88b.
888    888 "Y888888 888     "Y88888 888  888 "Y888888  "Y888

Welcome to Hardhat v2.10.2

? What do you want to do? … 
▸ Create a JavaScript project
  Create a TypeScript project
  Create an empty hardhat.config.js
  Quit

Войти в полноэкранный режим Выйти из полноэкранного режима

Выберите Create a JavaScript project и позвольте ему установить все зависимости.

Далее, установим пакет hardhat-deploy, который делает работу с hardhat в 2 раза проще и веселее 👻

> yarn add -D hardhat-deploy
Войдите в полноэкранный режим Выйти из полноэкранного режима

И добавьте следующее утверждение в ваш hardhat.config.js:

require('hardhat-deploy');

Enter fullscreen mode Выйти из полноэкранного режима

Следующий,

yarn add -D @nomiclabs/hardhat-ethers@npm:hardhat-deploy-ethers ethers
Войти в полноэкранный режим Выйти из полноэкранного режима

Все изменения в hardhat.config.js можно найти в моем REPO.

Далее, создайте новый контракт в директории contracts, не стесняйтесь дать ему любое имя, я назову его LazyMint.sol.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
import "@openzeppelin/contracts/access/AccessControl.sol";

contract LazyMint is ERC721, ERC721URIStorage, Ownable, EIP712, AccessControl {

    error OnlyMinter(address to);
    error NotEnoughValue(address to, uint256);
    error NoFundsToWithdraw(uint256 balance);
    error FailedToWithdraw(bool sent);

    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    string private constant SIGNING_DOMAIN = "Lazy-Domain";
    string private constant SIGNING_VERSION = "1";

    event NewMint(address indexed to, uint256 tokenId);
    event FundsWithdrawn(address indexed owner, uint256 amount);

    struct LazyMintVoucher{
        uint256 tokenId;
        uint256 price;
        string uri;
        bytes signature;
    }

    constructor(address minter) ERC721("LazyMint", "MTK") EIP712(SIGNING_DOMAIN, SIGNING_VERSION) {
        _setupRole(MINTER_ROLE, minter);
    }


    function mintNFT(address _to, LazyMintVoucher calldata _voucher) public payable {
        address signer = _verify(_voucher);
        if(hasRole(MINTER_ROLE, signer)){
            if(msg.value >= _voucher.price){
                _safeMint(_to, _voucher.tokenId);
                _setTokenURI(_voucher.tokenId, _voucher.uri);
                emit NewMint(_to, _voucher.tokenId);
            }else{
                revert NotEnoughValue(_to, msg.value);
            }
        }else{
            revert OnlyMinter(_to);
        }
    }

    function _hash(LazyMintVoucher calldata voucher) internal view returns(bytes32){
        return _hashTypedDataV4(keccak256(abi.encode(
            //function selector
            keccak256("LazyMintVoucher(uint256 tokenId,uint256 price,string uri)"),
            voucher.tokenId,
            voucher.price,
            keccak256(bytes(voucher.uri))
        )));
    }

    function _verify(LazyMintVoucher calldata voucher) internal view returns(address){
        bytes32 digest = _hash(voucher);
        //returns signer
        return ECDSA.recover(digest, voucher.signature);
    }

    // The following functions are overrides required by Solidity.

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId) public view virtual override(AccessControl, ERC721) returns (bool){
        return ERC721.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId);
    }

    function withdrawFunds() public payable onlyOwner{
        uint256 balance = address(this).balance;
        if(balance <= 0){revert NoFundsToWithdraw(balance);}
        (bool sent,) = msg.sender.call{value: balance}("");
        if(!sent){revert FailedToWithdraw(sent);}
        emit FundsWithdrawn(msg.sender, balance);
    }
}
Войдите в полноэкранный режим Выйти из полноэкранного режима

Давайте быстро рассмотрим контракт LazyMint.sol.
Я использую ERC721 & ERC721URIStorage из OpenZeppelin Contracts

ERC721 — это стандарт для представления прав собственности на не сгибаемые токены, то есть, где каждый идентификатор токена уникален.

ERC721URIStorage — это реализация ERC721, включающая расширения стандарта метаданных (IERC721Metadata), а также механизм метаданных для каждого токена.

Проще говоря, контракт, реализованный без ERC721Storage, генерирует tokenURI для tokenId на лету путем конкатенации baseURI + tokenID. В случае контрактов, использующих ERC721Storage, мы предоставляем tokenURI (метаданные) при майнинге токена, который затем хранится на цепи.

Далее, я использую Ownable & AccessControl.
Ownable — обеспечивает эксклюзивный доступ к функциям по контракту, например, только владелец имеет доступ к определенным функциям.

AccessControl — позволяет нам назначать определенные роли адресам, как в нашем случае, определенный адрес может подписывать ваучеры для чеканки NFTs, для этого мы можем создать роль под названием MINTER. или администратор для всего контракта.

Далее, я определил некоторые пользовательские ошибки, которые были введены в solidity версии 0.8.4.

 error OnlyMinter(address to);
 error NotEnoughValue(address to, uint256);
 error NoFundsToWithdraw(uint256 balance);
 error FailedToWithdraw(bool sent);
Вход в полноэкранный режим Выйти из полноэкранного режима

Далее мы определяем роль майнера, домен подписания & версию.
Чтобы при майнинге мы могли проверить, что подписанный ваучер, о котором говорилось выше, был подписан адресом майнера и включает тот же домен и версию.

 bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
    string private constant SIGNING_DOMAIN = "Lazy-Domain";
    string private constant SIGNING_VERSION = "1";
Вход в полноэкранный режим Выход из полноэкранного режима

Далее рассмотрим ваучер, который мы определили в Solidity с помощью struct.

struct LazyMintVoucher{
   uint256 tokenId;
   uint256 price;
   string uri;
   bytes signature;
}
Войти в полноэкранный режим Выйти из полноэкранного режима

Он включает в себя tokenId для монетного двора, цену, uri[обычно это url, указывающий на метаданные] для nft, и подпись. Давайте разберем функцию mintNFT, чтобы понять, как этот ваучер делает всю магию 🪄.

function mintNFT(address _to, LazyMintVoucher calldata _voucher) public payable {
  address signer = _verify(_voucher);
  if(hasRole(MINTER_ROLE, signer)){
     if(msg.value >= _voucher.price){
        _safeMint(_to, _voucher.tokenId);
        _setTokenURI(_voucher.tokenId, _voucher.uri);
        emit NewMint(_to, _voucher.tokenId);
     }else{
         revert NotEnoughValue(_to, msg.value);
     }
  }else{
     revert OnlyMinter(_to);
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

Важной частью ваучера является подпись, которую мы подписываем вне цепи с помощью закрытого ключа минтера, который содержит все дополнительные данные в том же порядке, который определен в приведенной выше struct.Затем в функции mintNFT, которая ожидает два аргумента _to & _voucher.
nft будет отчеканен для адреса _to. А ваучер в основном помогает погасить NFT. Первая строка в функции mintNFT — проверка подписи из ваучера. для проверки подписи мы вызываем криптографическую функцию из draft-EIP712. sol под названием _hashTypedDataV4, которая принимает нашу хэшированную версию структуры ваучера, а возвращаемое значение затем может быть использовано с функцией recover из Elliptic Curve Digital Signature Algorithm (ECDSA) для получения адреса подписанта. Затем мы сравниваем восстановленный адрес подписанта, чтобы проверить, совпадает ли он с нашим адресом MINTER, а также проверяем, имеет ли этот подписант роль minter_role. Если да, то мы продолжаем и проверяем, что значение (eth passed) совпадает со значением цены, указанным в ваучере. Если да ? мы продолжаем и майним токен и выдаем событие emit NewMint(_to, _voucher.tokenId).

Вот и все для магического трюка проверки ваучера 🪄🪄🪄🪄.

Далее у нас есть withdrawFunds, который позволяет только владельцу контракта вывести средства, если таковые имеются.

Как подписать ваучер?

Откройте createSalesOrder, перейдите в папку scripts, откройте createSalesOrder.js, который является простым скриптом для создания ваучеров.

Сначала мы получим учетную запись подписанта.

const [signer] = await ethers.getSigners();

Войдите в полноэкранный режим Выйдите из полноэкранного режима

Далее нам нужен домен подписи & версии, эти значения должны совпадать с теми, которые определены в контракте.

const SIGNING_DOMAIN = "Lazy-Domain";
const SIGNING_VERSION = "1";
const MINTER = signer;
Войти в полноэкранный режим Выйти из полноэкранного режима

В соответствии с EIP 712 нам нужен домен, который состоит из chainId, ContractAddress, domain & version.

const domain = {
  name: SIGNING_DOMAIN,
  version: SIGNING_VERSION,
  verifyingContract: lazyMint.address,
  chainId: network.config.chainId
}    
let voucher = {
  tokenId: i,
  price: PRICE,
  uri: META_DATA_URI
}
Войти в полноэкранный режим Выход из полноэкранного режима

META_DATA_URI я уже прикрепил его к ipfs

`ghost_metadata.json`
{
    "description": "Boooooo",
    "image": "https://ipfs.io/ipfs/QmeueVyGRuTH939fPhGcPC8iF6HYhRixGBRmEgiZqFUvEW",
    "name": "Baby Ghost",
    "attributes": [
        {
            "trait_type": "cuteness",
            "value": 100
        }
    ]
}
Войти в полноэкранный режим Выход из полноэкранного режима

Далее нам понадобится функция createVoucher, открытая voucherHelper.js. Для создания подписанного ваучера нам нужны три аргумента domain voucher types. Теперь types — это не что иное, как ваучер struct, который мы определили в смарт-контракте solidity. Убедитесь, что имя struct & и порядок переменных совпадают.

const types = {
    LazyMintVoucher:[
        {name: "tokenId", type:"uint256"},
        {name: "price", type:"uint256"},
        {name: "uri", type:"string"}
    ]
}
Вход в полноэкранный режим Выход из полноэкранного режима

Далее подписываем ваучер с помощью кошелька MINTER(signer) & получаем подписанный ваучер 🖋️.

const signature = await signer._signTypedData(domain, types, voucher);
Войти в полноэкранный режим Выйти из полноэкранного режима

В createSalesOrder.js в конце, как только я получу подписанные ваучеры, В моем случае, чтобы все было просто, я просто сохраняю подписанные ваучеры в файл под названием NFTVouchers.json, который я создаю прямо в моем dapp '.../lazymintdapp/src/NFTVouchers.json'. В идеале, в реальном сценарии вы храните эти подписанные ваучеры в вашей централизованной БД 🤪.

Готово! 🎉🎉

Тесты контрактов

Я написал несколько модульных тестов, которые можно найти в test/LazyMintUnit.test.js

> yarn hardhat test
Войти в полноэкранный режим Выход из полноэкранного режима

Развернутый контракт & демонстрация

https://ghost-mint.vercel.app/

https://mumbai.polygonscan.com/address/0x3077B8941e6337091FbB2E3216B1D5797B065C71

CODE REPO Не забудьте закинуть ⭐⭐⭐⭐⭐

LazyNFTs Dapp

Я не буду полностью разбирать дапп, но для разработки reactjs даппа я использовал create-react-app.

Пакеты

  • Wagmi: https://wagmi.sh/ для взаимодействия с контрактами, крутые и простые хуки для react. И в их документации есть много примеров, что значительно облегчает реализацию. 🦍

Ресурсы

Существует множество способов, которыми можно подойти к ленивому майнингу. Это был мой опыт обучения. Вот некоторые ресурсы, которые помогли мне понять ленивый майнинг.

  • https://www.alchemy.com/overviews/lazy-minting
  • https://eips.ethereum.org/EIPS/eip-712
  • https://medium.com/metamask/eip712-is-coming-what-to-expect-and-how-to-use-it-bb92fd1a7a26
  • https://nftschool.dev/tutorial/lazy-minting

Вот и все о ленивом майнинге. 👻

Оцените статью
devanswers.ru
Добавить комментарий