Да, я знаю, многие другие руководства могут рассказать вам, как вы можете создать свою торговую площадку NFT с помощью Solidity и Polygon. Но сегодня я хочу показать вам мой туториал — где мы создадим нашу торговую площадку с помощью Solidity, Hardhat и Polygon 😎

Установка инструментов

Возможно, кто-то, кто читает эту статью, уже знает Solidity, но не знает, что такое Hardhat, и не знает, какие инструменты мы будем здесь использовать. Вы можете пропустить этот раздел, если вы уже установили Hardhat, и узнать, как использовать этот замечательный инструмент.

Для начала создадим новую папку с проектом с терминалом:

$ mkdir nft-marketplace
$ cd nft-marketplace

Следующим шагом будет установка библиотеки Hardhat с помощью следующей команды:

$ npm install --save-dev hardhat

При запуске этой команды hardhat откроет меню установки нового проекта hardhat:

Теперь вам нужно выбрать здесь первый вариант и дождаться, когда будет создан новый проект каски. Когда генерация проекта будет завершена, пора приступать к разработке.

Приступаем к написанию договора

Теперь давайте откроем этот проект в Visual Studio Code, а также установим библиотеку OpenZeppelin в терминал:

$ npm install @openzeppelin/contracts

Что такое библиотека контрактов OpenZeppelin? Если мы хотим создавать быстрые токены NFT с нашим смарт-контрактом — нам нужно использовать эту библиотеку, потому что эта библиотека предоставляет все стандарты токенов. Но об этом мы поговорим подробнее в ближайшее время. Теперь переходим в папку «контракты», здесь вы видите контракт Greeter.sol — вы можете удалить этот контракт, так как он был создан по умолчанию. Теперь создайте новый файл с именем «NFT.sol» и откройте его. Во-первых, давайте определим идентификатор лицензии:

// SPDX-License-Identifier: MIT

Эта строка определяет лицензию нашего контракта, далее давайте определим версию Solidity, которую мы хотим использовать в нашем контракте:

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

Я только что определил, что мы хотим использовать версию 0.8.4 в нашем контракте, теперь давайте определим тело контракта, а также импортируем стандарт токена ERC721 из библиотеки OpenZeppelin:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
contract FrogToken { // I named our contract - FrogToken
  // Here we gonna write all code of the contract
}

Хорошо, теперь, когда мы импортировали стандарт токена ERC721 — пришло время использовать этот стандарт в нашем контракте следующим образом:

// ...
contract FrogToken is ERC721 {
  // Here we gonna write all code of the contract
}

Отлично, но это еще не все — теперь нам нужно создать конструктор и определить имя и символ нашего токена NFT:

// ...
contract FrogToken is ERC721 {
  constructor() ERC721("FrogToken", "FT") {
  }
}

Итак, теперь имя нашего токена будет FrogToken, а символ — FT. Отлично, следующий шаг — создать переменную с именем tokenCounter:

// ...
contract FrogToken is ERC721 {
  uint public tokenCounter;
  constructor() ERC721("FrogToken", "FT") {}
}

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

// ...
contract FrogToken is ERC721 {
  uint public tokenCounter;
  
  constructor() ERC721("FrogToken", "FT") {}
  function mintToken(string memory _tokenURI) public {
  }
}

Зачем нам здесь нужна переменная _tokenURI? Когда мы собираемся чеканить новый токен — нам также нужно получить метаданные из URI токена, и именно поэтому мы добавили эту переменную здесь. Отлично, прежде чем мы напишем код для функции mintToken, нам нужно определить значение переменной tokenCounter равным нулю:

// ...
contract FrogToken is ERC721 {
  uint public tokenCounter;
  constructor() ERC721("FrogToken", "FT") {
    tokenCounter = 0;
  }
  function mintToken(string memory _tokenURI) public {}
}

Отлично, теперь давайте напишем функцию монетного двора:

// ...
contract FrogToken is ERC721 {
  uint public tokenCounter;
  constructor() ERC721("FrogToken", "FT") {
    tokenCounter = 0;
  }
  function mintToken(string memory _tokenURI) public {
    _safeMint(msg.sender, tokenCounter);
    _setTokenURI(tokenCounter, msg.sender);
    tokenCounter++;
  }
}

Теперь позвольте мне объяснить код этой функции. Во-первых, мы используем функцию _safeMint — для чеканки нового токена на адрес отправителя. И отправитель получит токен с идентификатором, равным текущему значению переменной tokenCounter.

После этого мы используем функцию _setTokenURI, где мы определяем URI токена токена, который имеет идентификатор переменной tokenCounter. Наконец, мы увеличиваем значение значения tokenCounter, а это означает, что в следующей чеканке — следующий отправитель получит токен с идентификатором, равным 1.

Напишите тест для нашего контракта

Теперь пришло время протестировать наш контракт и проверить, как теперь работает функция монетного двора. Итак, давайте перейдем в папку «tests» и создадим новый файл, который будет называться NftTest.js:

const { expect } = require("chai");
describe("Deploy contract and mint token", function() {
  let deployer; // owner of the contract
  let FrogToken;
  let frogToken; // here will be deployed contract
  beforeEach(async function () {
    [deployer] = await ethers.getSigners();
    FrogToken = await ethers.getContractFactory("FrogToken");
    
    frogToken = await FrogToken.deploy();
  });
  it("Test mint token", async function() {
    var tx = await frogToken.mintToken("http://my-json-server.typicode.com/abcoathup/samplenft/tokens/0");
    await tx.wait(1);
    console.log("Here is you mint transaction: ", tx);
  });
});

Отлично, теперь давайте запустим следующую команду для проверки нашего контракта:

$ npx hardhat test

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

// ...
contract FrogToken is ERC721 {
  // ...
  mapping(address => Token[]) public mintedTokens;
  struct Token {
    uint256 tokenId;
    string tokenURI;
    address payable owner;
  }
  // ...
  function getMintedToken(address _owner) public view returns(Token[]) {
    return mintedTokens[_owner];
  }
}

Что мы здесь видим? Во-первых, я создал сопоставление, которое будет сохранять все mintedTokens некоторых пользователей. Второе, что я создал — это структура Token, которую мы будем использовать для сохранения информации о выпущенных пользователем токенах. И последнее — это функция getMintedToken(), которая получает токены, отчеканенные пользователем. Теперь давайте перейдем к функции mintToken() и добавим некоторые изменения:

// ...
  function mintToken(string memory _tokenURI) public {
    _safeMint(msg.sender, tokenCounter);
    _setTokenURI(tokenCounter, msg.sender);
    mintedTokens[msg.sender] = Token(tokenCounter, _tokenURI, payable(msg.sender));   
    tokenCounter++;
  }
// ...

Как видите, мы только что добавили небольшой фрагмент кода, который добавит отчеканенные токены в сопоставление mintedTokens(). Это круто — потому что теперь мы можем получить информацию о том, какие токены чеканили те или иные пользователи. Прохладный! Теперь напишем еще один тест, откроем NftTest.js:

const { expect } = require("chai");
describe("Deploy contract and mint token", function() {
  let deployer; // owner of the contract
  let FrogToken;
  let frogToken; // here will be deployed contract
  beforeEach(async function () {
    [deployer] = await ethers.getSigners();
    FrogToken = await ethers.getContractFactory("FrogToken");
    
    frogToken = await FrogToken.deploy();
  });
  it("Test mint token", async function() {
    var tx = await frogToken.mintToken("http://my-json-server.typicode.com/abcoathup/samplenft/tokens/0");
    await tx.wait(1);
    console.log("Here is you mint transaction: ", tx);
  });
  it("Test getMintedToken()", async function() {
    var token = await frogToken.getMintedToken(msg.sender);
    console.log( token );
  });
});

Хорошо, теперь давайте запустим этот тест, и посмотрим результат, что мы получим токен. Итак, давайте сделаем это:

$ npx hardhat test

И что мы видим в терминале? Мы видим результат нашего теста, это хэш транзакции и объект токена. Но проверять результат мы не утверждали, пока можно сказать, что проверяли только функцию «getMintedToken». Это хорошо, и следующее, что нам нужно сделать — это написать скрипт для развертывания нашего контракта в тестовой сети (например, Rinkeby).

Развертывание в сети Rinkeby

Прежде чем мы развернем наш контракт в сети Rinkeby, вам нужно иметь расширение кошелька metamask (но я думаю, что у людей, которые уже читают эту статью, это расширение уже есть и они знают, как им пользоваться). А также вам нужно будет создать кошелек и переключиться на тестовую сеть с именем rinkeby:

И вы будете делать все эти шаги, вам нужно будет получить немного тестового эфира на кран ринкеби. Инструкции по получению тестового эфира вы найдете на странице сборщика rinkeby. А теперь вам нужно создать учетную запись Алхимия. Почему? Нам это понадобится, когда мы собираемся развернуть контракт, поэтому создайте учетную запись, а после этого — вам нужно будет создать новое приложение:

И когда вы будете создавать новое приложение — важно выбрать, что мы хотим использовать сеть Rinkeby:

Отлично, теперь давайте откроем ключи проекта, нажав на кнопку «Просмотреть ключи». Здесь вам нужно будет скопировать URL-адрес приложения:

Скопируйте URL-адрес вашего приложения, и нам нужно будет использовать его в конфигурации HardHat. Итак, теперь давайте откроем наш редактор кода и откроем файл hardhat.config.js:

/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
 solidity: "0.8.4",
 networks: {
  rinkeby: {
   url: 'your-app-url',
   accounts ["your-wallet-secret"]
  }
 },
};

Если вы не знаете, где взять секретный ключ кошелька метамаски — вот небольшая инструкция. Сначала откройте данные своего кошелька:

Откроется окно, в котором вы можете получить секретный ключ своего кошелька после ввода пароля своей учетной записи:

Скопируйте секретный ключ и вставьте в файл конфигурации. Теперь у нас есть все, что нужно для развертывания нашего контракта, поэтому давайте перейдем в папку scripts и создадим новый файл с именем deploy.js:

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("FrogToken");
 const token = await Token.deploy();
 console.log("Token address:", token.address);
}
main()
 .then(() => process.exit(0))
 .catch((error) => {
  console.error(error);
  process.exit(1);
 });

Хорошо сделано. Теперь давайте откроем терминал и запустим следующую команду для развертывания этого контракта:

$ npx hardhat run scripts/deploy.js --network rinkeby

В терминале вы увидите следующее:

Если вы скопируете адрес токена и зайдете на rinkeby etherscan — вы обнаружите, что ваш контракт развернут:

Потрясающе 💥 Это действительно отличный результат. В следующей части этой статьи — мы напишем первую версию нашего приложения — куда мы добавим наши токены и покажем их. Скоро мы также обновим код контракта и сделаем его более динамичным с большим количеством функций. На этом все — спасибо за прочтение 😎 и до встречи в следующей части статьи