Игра Яндекс Практикума
Игра Яндекс Практикума
Игра Яндекс Практикума

Написание смарт-контракта для NFT 

Отредактировано

Привет! Меня зовут Костанян Карен, я занимаюсь разработкой на Node.js в цифровом интеграторе Secreate. В этой статье мы разберемся как написать смарт контракт и отчеканить наши нфт.

19К открытий19К показов

Привет! Меня зовут Костанян Карен, я занимаюсь разработкой на Node.js в цифровом интеграторе Secreate. В этой статье мы разберемся как написать смарт контракт и отчеканить наши нфт.

Если у вас нет медиафайлов и метаданных NFT в формате JSON, мы создали коллекцию изображений, с которыми вы можете поэкспериментировать. Вы можете найти медиафайлы здесь и файлы метаданных JSON здесь.

Мы будем использовать Hardhat, стандартную среду разработки Ethereum, для разработки, развертывания и проверки наших смарт-контрактов. Создайте пустую папку для нашего проекта и инициализируйте пустой файл package.json, выполнив в терминале следующую команду:

			mkdir my-nft && cd my-nft && npm init -y
		

Теперь вы должны находиться в папке my-nft и иметь файл с именем package.json. Далее давайте установим Hardhat. Выполните следующую команду:

			npm install --save-dev hardhat
		

После установки hardhat, мы можем создать пример проекта Hardhat, выполнив следующую команду:

			npx hardhat
		

После этой команды, мы увидим несколько пунктов для выбора, выберите пункт (Create a basic sample project) и согласитесь со всем по умолчанию.

Давайте проверим, правильно ли установлен наш пример проекта. Выполните следующую команду:

			npx hardhat run scripts/sample-script.js
		

Если все прошло успешно, вы должны увидеть следующее, отличия может быть адрес контракта:

Написание смарт-контракта для NFT  1

Теперь у нас есть успешно настроенная среда разработки hardhat.

Далее, давайте установим пакет контрактов OpenZeppelin. Это даст нам доступ к контрактам ERC721 (стандарт для NFT).

			npm install @openzeppelin/contracts
		

Если мы хотим поделиться кодом нашего проекта публично (на веб-сайте, таком как GitHub), и при этом мы не хотим делиться конфиденциальной информацией, такой как наш закрытый ключ,  API Etherscan или наш URL-адрес Alchemy (не беспокойтесь, если некоторые из этих слов вам пока не понятны), давайте установим другую библиотеку с именем dotenv.

			npm install dotenv
		

Теперь мы установили все зависимости и можем приступить написанию нашего контракта.

Написание смарт-контракта

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

Вот как будет сначала выглядит наш контракт:

			pragma solidity ^0.8.9;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract MyNft is ERC721Enumerable, Ownable {
uint public constant PRICE = 0.005 ether;
string public baseTokenURI;
mapping (address => uint[]) nftOwner;
event MintNft(address senderAddress, uint256 nftToken);
}
		

1. pragma solidity ^0.8.9

Весь исходный код Solidity должен начинаться с объявления версии компилятора Solidity, которую должен использовать этот код. Это делается для предотвращения проблем с будущими версиями компилятора, которые потенциально могут внести изменения, которые могут нарушить ваш код.

2. import

Если у вас есть несколько файлов и вы хотите импортировать один файл в другой, Solidity использует ключевое слово import.

3. PRICE

Цена для чеканки(mint) одного NFT.

4. baseTokenURI

URL-адрес IPFS папки, содержащей метаданные JSON.

5. mapping

Это одно из видов хранилища данных (ключей и значений) который называется Storage, здесь мы будет хранить все токены принадлежащего пользователя.

6. event

Это способ нашего контракта сообщает интерфейсу вашего приложения о том, что что-то произошло в блокчейне, и может прослушивать определенные события и предпринимать действия, когда они происходят.

Далее, мы установим baseTokenURI в нашем конструкторе. Мы также вызовем родительский конструктор(ERC721) и установим имя и символ для наших NFT.

Таким образом, наш конструктор выглядит так:

			constructor(string memory baseURI)  ERC721("My nft", "NFT") {
      setBaseURI(baseURI);
 }
Настройка URI для базового токена

function _baseURI() internal  view  virtual  override returns (string memory) {
     return baseTokenURI;
}

function setBaseURI(string memory _baseTokenURI) public onlyOwner {
     baseTokenURI = _baseTokenURI;
}

Наши метаданные NFT JSON доступны по этому URL-адресу IPFS:
ipfs:/QmTMo6DFrfzKGGbkYsyMZRe16jBJcCcV72ZJHcM3a3Z2w7/
		

Когда мы устанавливаем его в качестве базового URI, реализация OpenZeppelin автоматически выводит URI для каждого токена. Предполагается, что метаданные токена 1 будут доступны по адресу

ipfs:/QmTMo6DFrfzKGGbkYsyMZRe16jBJcCcV72ZJHcM3a3Z2w7/1,

а метаданные токена 2 будут доступны по адресу ipfs:/QmTMo6DFrfzKGGbkYsyMZRe16jBJcCcV72ZJHcM3a3Z2w7/2

и так далее.

Однако нам нужно сообщить нашему контракту, что переменная baseTokenURI, которую мы определили, является базовым URI, который должен использовать контракт. Для этого мы переопределяем пустую функцию _baseURI() и возвращаем baseTokenURI.

Мы также пишем функцию для владельца, которая позволяет владельцу изменять baseTokenURI даже после развертывания контракта.

Резервация nft для владельца

Как владелец, вы можете зарезервировать несколько NFT для себя бесплатно (оплату надо сделать только за газ).

			function reserveNFT(uint _tokenId) public onlyOwner {
        _safeMint(msg.sender, _tokenId);
        nftOwner[msg.sender].push(_tokenId);
        emit MintNft(msg.sender, _tokenId);
}
		

Здесь мы определяем публичную функцию reserveNFT, которая принимает один параметр _tokenId. Это и будет наш токен который мы хотим чеканить и зарезервировать для себя. Обратите внимание, что у этого параметра есть нижнее подчеркивание (_). Он не обязателен, но есть не прописанное внутреннее правило, что все входящие параметры мы пишем со знаком _;

1. _safeMint

Это та самая чудесная функция которая делает чеканку, ее мы берем из контракта ERC721.

2. nftOwner.

Сохраняем кому принадлежит этот токен.

3. emit

Запускаем наше событие, что пользователь (msg.sender) сделал чеканку (_tokenId)

Функция Mint NFT

Пришло время заработать немного денег. Для того чтобы пользователи могли чеканить наши nft они должны вызвать функцию mintNFT.

			function mintNFT(uint _tokenId) public payable {
     require(msg.value >= PRICE, "Not enough ether to purchase NFT.");
     _safeMint(msg.sender, _tokenId);
    emit MintNft(msg.sender, _tokenId);
}
		

1. payable

Это модификатор, который дает знать что функция подлежит к оплате и пользователь отправляет эфир.

2. require:

Проверяет чтобы отправленный эфир был достаточным для чеканки nft.

Получение всех токенов, принадлежащих определенному пользователю.

			function tokensOfOwner(address _owner) external view returns (uint[] memory) {
    return nftOwner[_owner];
}
		

Функция принимает адрес пользователя и возвращает токены которые мы хранили в хранилище nftOwner.

Вывод баланса

Самая прекрасная функция. Ее запуск, отправляет на кошелек владельца все заработанные эфиры.

			function withdraw() public payable onlyOwner {
    uint balance = address(this).balance;
    require(balance > 0, "No ether left to withdraw");
    (bool success, ) = (msg.sender).call{value: balance}("");
    require(success, "Transfer failed.");
}
		

Функцию может запустить только владелец.

1. balance – выбирает всю сумму на балансе контракта.

2. Проверяет чтобы баланс был положительным.

3. Отправляет эфир на кошелек владельца.

4. Проверяет что трансфер прошел успешно, в противном случае идет откат транзакции.

Проверка токена (modifier)

Если вы заметили, наши функции для чеканки reserveNFT, mintNFT могут чеканить токен который уже ранее уже был отчеканен. И перед началом процесса, мы не проверяем был ли данный токе уже отчеканен или нет. Для этого мы создадим модифаер который будет проверять статус токена до чеканки.

			modifier checkTokenStatus(uint _tokenId) {
    bool isTokenSold = false;
    for (uint i = 0; i < soldedTokenIds.length; i++) {
        if(soldedTokenIds[i] == _tokenId) {
            isTokenSold = true;
            break;
        }
    }
    require(!isTokenSold, "Token is sold");
    _;
}
		

Модификатор функции выглядит так же как функция, но использует ключевое слово modifier вместо function. Его нельзя вызвать напрямую как функцию, вместо этого мы можем присоединить имя модификатора в конце определения функции, чтобы изменить ее поведение.

В modifier мы проверяем отчеканен ли переданный на чеканку токен. А все уже отчеканенные токены мы будем хранить в хранилище soldedTokenIds.

Самую главную роль здесь играет нижнее подчеркивание, которое находится в последней строке у модификатора. Оно вызывает остальную часть функции, на которую был вызван модификатор checkTokenStatus.

Для того чтобы наш модификатор начал правильно функционировать, мы должны добавить одно хранилище (Storage) soldedTokenIds и немного отредактировать наши функции для чеканки reserveNFT и mintNFT.

1. В самом верху контракт добавляем наше хранилище.

			uint public constant PRICE = 0.005 ether;
string public baseTokenURI;
uint[] soldedTokenIds; ---- new
mapping (address => uint[]) nftOwner;
event MintNft(address senderAddress, uint256 nftToken);
		

2. Отредактируем наши функции reserveNFT и mintNFT.

			function reserveNFT(uint _tokenId) public onlyOwner checkTokenStatus(_tokenId) {
    _safeMint(msg.sender, _tokenId);
    nftOwner[msg.sender].push(_tokenId);
    soldedTokenIds.push(_tokenId); ---- new
    emit MintNft(msg.sender, _tokenId);
}
		
			function mintNFT(uint _tokenId) public payable checkTokenStatus(_tokenId) {
    require(msg.value >= PRICE, "Not enough ether to purchase NFTs.");
    _safeMint(msg.sender, _tokenId);
    soldedTokenIds.push(_tokenId); ---- new
    emit MintNft(msg.sender, _tokenId);
}
		

После чеканки мы храним ид токен в хранилище soldedTokenIds, и в конце функции мы добавили наш модификатор с параметром который получает наша функция.

Вот мы закончили с нашим смарт контрактом, он должен выглядеть следующим образом.

			//SPDX-License-Identifier: MIT
pragma solidity ^0.8.9;
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";

contract MyNft is ERC721Enumerable, Ownable {

    uint public constant PRICE = 0.005 ether;
    string public baseTokenURI;
    uint[] soldedTokenIds;
    mapping (address => uint[]) nftOwner;
    event MintNft(address senderAddress, uint256 nftToken);

    constructor(string memory baseURI)  ERC721("My nft", "NFT") {
        setBaseURI(baseURI);
    }

    function _baseURI() internal view virtual override returns (string memory) {
        return baseTokenURI;
    }

    function setBaseURI(string memory _baseTokenURI) public onlyOwner {
        baseTokenURI = _baseTokenURI;
    }

     modifier checkTokenStatus(uint _tokenId) {
        bool isTokenSold = false;
        for (uint i = 0; i < soldedTokenIds.length; i++) {
            if(soldedTokenIds[i] == _tokenId) {
                isTokenSold = true;
                break;
            }
        }
        require(!isTokenSold, "Token is sold");
        _;
    }

    function reserveNFT(uint _tokenId) public onlyOwner checkTokenStatus(_tokenId) {
        _safeMint(msg.sender, _tokenId);
        nftOwner[msg.sender].push(_tokenId);
        soldedTokenIds.push(_tokenId);
        emit MintNft(msg.sender, _tokenId);
    }

    function mintNFT(uint _tokenId) public payable checkTokenStatus(_tokenId) {
        require(msg.value >= PRICE, "Not enough ether to purchase NFTs.");
        _safeMint(msg.sender, _tokenId);
        soldedTokenIds.push(_tokenId);
        emit MintNft(msg.sender, _tokenId);
    }

    function tokensOfOwner(address _owner) external view returns (uint[] memory) {
        return nftOwner[_owner];
    }

    function withdraw() public payable onlyOwner {
        uint balance = address(this).balance;
        require(balance > 0, "No ether left to withdraw");
        (bool success, ) = (msg.sender).call{value: balance}("");
        require(success, "Transfer failed.");
    }
}
		

Развертывание контракта

Для того чтобы мы могли посмотреть как работает наш контракт, мы должны развернуть его на тестовых сетях, например на Rinkeby.

Примечание. Советую развернуть его на сети Rinkeby, чтобы мы могли посмотреть результат на платформе opensea, так как из других сетей на данный момент это будет невозможно.

Для развертывания нам понадобятся следующие вещи. URL-адрес RPC, закрытый ключ от кошелька и апи ключ от etherscan.io

1. Нам понадобится URL-адрес RPC, который позволит транслировать нашу транзакцию создания контракта. Мы будем использовать Алхимию. Создайте учетную запись в Alchemy, потом необходимо создать приложение (это бесплатно).

Написание смарт-контракта для NFT  2

Пишем любое название, CHAIN: Ethereum, NETWORK: Rinkeby

После того как приложение создано, перейдите на панель инструментов Alchemy и выберите его. Откроется новое окно с кнопкой View Key в правом верхнем углу. Нажмите на кнопку и выберите URL-адрес HTTP.

2. Для того чтобы получить приватный ключ от кошелька, откройте расширение Metamask, нажмите на троеточие в правом углу, и в открывшемся окне нажмите на “Реквизиты счета”, затем на кнопку “Экспортировать закрытый ключ”.

После получения HTTP-адреса и закрытого ключа от кошелька, мы их пропишем в .env файл.

			// .env
API_URL = "<--URL-адрес RPC-->"
PRIVATE_KEY = "<--закрытый ключ от кошелька-->"
		

Теперь замените файл hardhat.config.js следующим содержимым.

			require("@nomiclabs/hardhat-waffle");
require('dotenv').config();
const { API_URL, PRIVATE_KEY } = process.env;


task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.9",
  defaultNetwork: "rinkeby",
  networks: {
    rinkeby: {
      url: API_URL,
      accounts: [PRIVATE_KEY]
    }
  },
};
		

Затем создадим файл scripts/run.js со следующим содержимым. IPFS URL замените со своим ipfs-ом (пример: ipfs://some_token/) или можете использовать наш ipfs который был описан выше.

			const { utils } = require("ethers");

async function main() {
    const baseTokenURI = "<--IPFS URL-->";

    // Get owner/deployer's wallet address
    const [owner] = await hre.ethers.getSigners();

    // Get contract that we want to deploy
    const contractFactory = await hre.ethers.getContractFactory("MyNft");

    // Deploy contract with the correct constructor arguments
    const contract = await contractFactory.deploy(baseTokenURI);

    // Wait for this transaction to be mined
    await contract.deployed();

    // Get contract address
    console.log("Contract deployed to:", contract.address);
}

main()
    .then(() => process.exit(0))
    .catch((error) => {
        console.error(error);
        process.exit(1);
    });
		

Мы уже готовы для разворачивания нашего смарт контракта, но для этого нам потребуются деньги на нашем счете.

Фейковый эфир, вы можете взять отсюда:

Теперь когда у вас на счете есть деньги, нам надо развернуть наш контракт, для этого нужно набрать следующую команду и так как это уже реальная транзакция в сети rinkeby, мы должны будем платить газ, вот для чего нам нужен был эфир на счете.

			npx hardhat clean
npx hardhat run scripts/run.js --network rinkeby
		

Если все прошло успешно вы должны увидеть адрес вашего контракта. В нашем случае:

0xd440a80F4845D5bf1AdD3868573e286e2Df04df0

Вы можете проверить этот контракт на Etherscan. Перейдите на Etherscan и введите адрес контракта. Вы должны увидеть что-то вроде этого.

Написание смарт-контракта для NFT  3

Нам осталось верифицировать наш контракт, чтобы полноценно использовать его.

Для верификации нам понадобится ключ Etherscan API. Зарегистрируйте бесплатную учетную запись здесь и получите доступ к своим ключам API здесь. Добавим этот ключ API в наш файл .env.

			//.env
ETHERSCAN_API = "<--апи ключ от etherscan.io-->"
		

Установим следующий пакет для проверки нашего контракта.

npm install @nomiclabs/hardhat-etherscan

Теперь наш hardhat.config.js должен выглядеть следующим образом:

			require("@nomiclabs/hardhat-waffle");
require("@nomiclabs/hardhat-etherscan");
require('dotenv').config();
const { API_URL, PRIVATE_KEY, ETHERSCAN_API } = process.env;


// This is a sample Hardhat task. To learn how to create your own go to
// https://hardhat.org/guides/create-task.html
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
  const accounts = await hre.ethers.getSigners();

  for (const account of accounts) {
    console.log(account.address);
  }
});

// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more

/**
 * @type import('hardhat/config').HardhatUserConfig
 */
module.exports = {
  solidity: "0.8.9",
  defaultNetwork: "rinkeby",
  networks: {
    rinkeby: {
      url: API_URL,
      accounts: [PRIVATE_KEY]
    }
  },
  etherscan: {
    apiKey: ETHERSCAN_API
  }
};
		

Теперь выполните следующие две команды:

npx hardhat clean

На места DEPLOYED_CONTRACT_ADDRESS поставьте тот адрес контракта который получили ранее, на места BASE_TOKEN_URI поставьте ваш ipfs url.

В нашем случае вторая команда выглядела так:

			npx hardhat verify --network rinkeby 0xe1685d68829bc8E963332B29ebdcC8CA307cF3B8
 "ipfs://QmTMo6DFrfzKGGbkYsyMZRe16jBJcCcV72ZJHcM3a3Z2w7/"
		

Теперь, если вы посетите страницу Rinkeby Etherscan вашего контракта, вы должны будете увидеть маленькую зеленую галочку рядом с вкладкой Contract, которая подтверждает что ваш контракт верифицирован и ваши пользователи теперь смогут подключаться к web3 с помощью Metamask и вызывать функции вашего контракта из самого Etherscan! Попробуйте это сами.

Подключите учетную запись, которую вы использовали для развертывания контракта и вызовите функцию reserveNFT, потом можете попросить кого-нибудь сделать чеканку через функцию mintNFT.

После того как вы отчеканили nft через reserveNFT или через mintNFT, вы на стороне OpenSea должны увидеть ваши nft (testnets.opensea.io)

Написание смарт-контракта для NFT  4

После того как у вас в балансе у контракта появится эфир, только вы как владелец можете запустить функцию withdraw и после успешной транзакции на ваш кошелек зачислится весь баланс из контракта.

Заключение

Теперь у нас есть развернутый смарт-контракт, который позволяет пользователям чеканить NFT из нашей коллекции. Очевидным следующим шагом будет создание приложения web3, которое позволит нашим пользователям создавать NFT прямо с вашего веб-сайта.

Следите за новыми постами
Следите за новыми постами по любимым темам
19К открытий19К показов