Проект PetShop, день 2: Создание стандартного НМТ PetShop стандарта ERC721

Во второй день мы создадим простой ERC721 NFT с помощью OpenZeppelin.

Установите OpenZeppelin

Нам нужно установить обновляемый вариант библиотеки OpenZeppelin Contracts, которая позволяет нам легко создавать ERC721-совместимые НФТ в Solidity.

Нам также понадобится плагин OpenZeppelin Upgrades для Hardhat, который позволяет нам развертывать и обновлять прокси для наших контрактов на JavaScript.

$ npm install --save-dev 
    @openzeppelin/contracts-upgradeable 
    @openzeppelin/hardhat-upgrades
Вход в полноэкранный режим Выход из полноэкранного режима

Затем мы добавляем следующую строку в верхнюю часть нашего файла hardhat.config.js:

require('@openzeppelin/hardhat-upgrades');
Войти в полноэкранный режим Выйти из полноэкранного режима

Таким образом, в наших пользовательских задачах Hardhat экземпляр upgrades будет доступен в глобальной области видимости (как и ethers).

Создание контракта PetShop

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;

import "@openzeppelin/contracts-upgradeable/token/ERC721/extensions/ERC721URIStorageUpgradeable.sol";
import "@openzeppelin/contracts-upgradeable/utils/CountersUpgradeable.sol";

contract PetShop is ERC721URIStorageUpgradeable {
    using CountersUpgradeable for CountersUpgradeable.Counter;
    CountersUpgradeable.Counter private tokenIds;

    function initialize() initializer public {
        __ERC721_init("Pet Shop", "PET");
     }

    function mintToken(string calldata _tokenURI, address _to) external returns (uint256) {
        tokenIds.increment();
        uint256 newTokenId = tokenIds.current();
        _mint(_to, newTokenId);
        _setTokenURI(newTokenId, _tokenURI);
        return newTokenId;
    }
}
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Протестируйте контракт PetShop

Теперь создадим test/PetShop.js и добавим несколько тестов для нашего НФТ PetShop, который соответствует стандарту ERC721. В дополнение к методу mintToken(), который мы добавим, мы также протестируем некоторые методы ERC721, такие как tokenURI(), ownerOf() и balanceOf().

const { ethers, upgrades } = require("hardhat");
const { expect } = require("chai");
const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers");

// NOTE: We could also use "@openzeppelin/test-helpers".
// See: https://docs.openzeppelin.com/test-helpers/0.5/
const ZERO_ADDRESS = '0x0000000000000000000000000000000000000000';

describe("PetShop contract", function () {

  async function deployPetShopFixture() {
    const PetShop = await ethers.getContractFactory("PetShop");
    const accounts = await ethers.getSigners();

    // NOTE: This is an upgradeable contract which involves a proxy contract
    // and one or more logic contracts, so the way how it's deployed is a bit different.
    const petShop = await upgrades.deployProxy(PetShop);
    await petShop.deployed();
    return { PetShop, petShop, accounts };
  }

  describe("Deployment", function() {
    it("should initialize the NFT name and symbol", async function() {
      const { petShop } = await loadFixture(deployPetShopFixture);
      expect(await petShop.name()).to.equal("Pet Shop");
      expect(await petShop.symbol()).to.equal("PET");
    });
  });

  describe("Transactions", function() {
    it("should mint NFTs", async function() {
      const { petShop, accounts } = await loadFixture(deployPetShopFixture);

      const someAccounts = accounts.slice(1, 4);
      for (let i = 0; i < someAccounts.length; i++) {
        const account = someAccounts[i];
        const tokenID = i + 1; // Token ID should start from 1.
        const tokenURI = `https://petshop.example/nft/${tokenID}`;
        await expect(
          petShop.connect(account).mintToken(tokenURI, account.address)
        ).to.emit(petShop, "Transfer").withArgs(ZERO_ADDRESS, account.address, tokenID);
        expect(await petShop.tokenURI(tokenID)).to.equal(tokenURI);
        expect(await petShop.ownerOf(tokenID)).to.equal(account.address);
        expect(await petShop.balanceOf(account.address)).to.equal(1);
      }

      expect(await petShop.balanceOf(accounts[0].address)).to.equal(0);
    });
  });

});
Вход в полноэкранный режим Выход из полноэкранного режима

Чтобы запустить тест:

$ npx hardhat test test/PetShop.js
PetShop contract
    Deployment
      ✔ should initialize the NFT name and symbol (1824ms)
    Transactions
      ✔ should mint NFTs (289ms)
2 passing (2s)
Войдите в полноэкранный режим Выйти из полноэкранного режима

Создание простой задачи Hardhat

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

Обратите внимание, что среда выполнения Hardhat Runtime Environment будет доступна в глобальной области видимости. Используя плагин ether.js (который входит в состав Hardhat Toolbox) и плагин OpenZeppelin Upgrades, мы получаем доступ к экземплярам ethers и upgrades напрямую.

Давайте создадим tasks/petshop.js и добавим простую задачу balance для отображения баланса счета:

const { task } = require("hardhat/config");

task("balance", "Prints account's balance")
  .addOptionalParam("account", "The account's address")
  .setAction(async (taskArgs) => {
    let accounts = null;
    if (taskArgs.account) {
      accounts = [taskArgs.account];
    } else {
      console.log("Argument --account not provided: Showing all balances.");
      accounts = await ethers.getSigners();
    }
    for (const account of accounts) {
      const balance = await account.getBalance();
      const eth = ethers.utils.formatEther(balance);
      console.log(`${account.address} : ${eth} ETH`);
    }
  });
Войти в полноэкранный режим Выйти из полноэкранного режима

Чтобы включить нашу пользовательскую задачу в Hardhat, просто импортируйте наш файл задач в hardhat.config.js:

require("./tasks/petshop");
Войти в полноэкранный режим Выйти из полноэкранного режима

Попробуйте вызвать задачу balance:

$ npx hardhat balance
Argument --account not provided: Showing all balances.
0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 : 10000.0 ETH
0x70997970C51812dc3A010C7d01b50e0d17dc79C8 : 10000.0 ETH
...
Войти в полноэкранный режим Выход из полноэкранного режима

Добавление задачи для развертывания PetShop NFT

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

Добавьте новую задачу в тот же файл tasks/petshop.js, чтобы развернуть наш контракт PetShop NFT:

const { task } = require("hardhat/config");

// ... the `balance` task ...

const CONTRACT_NAME = "PetShop";

task("petshop-deploy", `Deploys the ${CONTRACT_NAME} NFT contract`)
  .setAction(async () => {
    const [deployer] = await ethers.getSigners();
    console.log(`Deployer: ${deployer.address} (balance: ${await deployer.getBalance()})`);

    const Contract = await ethers.getContractFactory(CONTRACT_NAME);
    const contract = await upgrades.deployProxy(Contract);
    await contract.deployed();
    console.log(`Deployed ${CONTRACT_NAME} at: ${contract.address}`);

    const name = await contract.name();
    const symbol = await contract.symbol();
    console.log(`Querying NFT: name = ${name}; symbol = ${symbol}`);
  });
Вход в полноэкранный режим Выйдите из полноэкранного режима

Откройте новый терминал и запустите узел демона сети Hardhat:

$ npx hardhat node
Started HTTP and WebSocket JSON-RPC server at http://127.0.0.1:8545/
...
Войти в полноэкранный режим Выйти из полноэкранного режима

Откройте другой терминал. Скомпилируйте и разверните наш контракт PetShop:

$ npx hardhat compile
Compiled 15 Solidity files successfully

$ npx hardhat petshop-deploy --network localhost
Deployer: 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 (balance: 9999996480306960525680)
Deployed PetShop at: 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707
Querying NFT: name = Pet Shop; symbol = PET
Войдите в полноэкранный режим Выйдите из полноэкранного режима

После успешного развертывания мы получим адрес контракта (0x5FC8d32690cc91D4c39d9d3abcBD16989F875707). Этот адрес понадобится нам в других задачах.

Добавление задачи по чеканке монет в PetShop NFT

Прежде чем добавлять другие задачи, я создам файл tasks/utils.js и добавлю в него несколько вспомогательных функций:

async function loadNFTContract(name, address) {
  const contract = await ethers.getContractAt(name, address);
  // We assume that the contract is ERC721 compliant.
  const nftName = await contract.name();
  const nftSymbol = await contract.symbol();
  console.log(`Loaded NFT contract ${name} from ${address}: ${nftName} (${nftSymbol})`);
  return contract;
}

async function executeTx(asyncTxFunc) {
  console.log('  * Sending tx...');
  const tx = await asyncTxFunc();
  console.log('  * Waiting tx to be mined...');
  const receipt = await tx.wait();
  console.log(`  * Tx executed, gas used: ${receipt.gasUsed}`);
  return receipt;
}

module.exports = {
  loadNFTContract,
  executeTx,
}
Вход в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, как мы загружаем контракт только по имени и адресу: Мы используем вспомогательный метод getContractAt(), добавленный к объекту ethers плагином hardhat-ethers.

Вернитесь к tasks/petshop.js. Чтобы отчеканить NFT PetShop и наградить им аккаунт:

const { task } = require("hardhat/config");
const { loadNFTContract, executeTx } = require("./utils");

const CONTRACT_NAME = "PetShop";

// ... the `balance` task ...

// ... the `petshop-deploy` task ...

task("petshop-mint", `Mints a ${CONTRACT_NAME} NFT to an account`)
  .addParam("address", "The contract address")
  .addParam("to", "The receiving account's address")
  .addParam("uri", "The token's URI")
  .setAction(async (taskArgs) => {
    const contract = await ethers.getContractAt(CONTRACT_NAME, taskArgs.address);
    const name = await contract.name();
    const symbol = await contract.symbol();
    console.log(`Loaded contract from ${taskArgs.address}: ${name} (${symbol})`);

    const accounts = await ethers.getSigners();
    const account = accounts.find(elem => elem.address === taskArgs.to);
    if (account === undefined) {
      throw new Error(`Could not find account with address: ${taskArgs.to}`);
    }

    const receipt = await executeTx(
      async () => contract.connect(account).mintToken(taskArgs.uri, account.address)
    );

    console.log("Looking for Transfer event from receipt...");
    const event = receipt.events.find(event => event.event === 'Transfer');
    const [from, to, tokenID] = event.args;
    console.log(`  event   = ${event.event}`);
    console.log(`  from    = ${from}`);
    console.log(`  to      = ${to}`);
    console.log(`  tokenID = ${tokenID}`);
  });
Войдите в полноэкранный режим Выйдите из полноэкранного режима

Чеканите токен и вручите его одному из наших тестовых аккаунтов:

$ npx hardhat petshop-mint --network localhost 
    --address 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 
    --to      0x70997970C51812dc3A010C7d01b50e0d17dc79C8 
    --uri     https://petshop.example/nft/foo/
Loaded contract from 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707: Pet Shop (PET)
  * Sending tx...
  * Waiting tx to be mined...
  * Tx executed, gas used: 111140
Looking for Transfer event from receipt...
  event   = Transfer
  from    = 0x0000000000000000000000000000000000000000
  to      = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
  tokenID = 1
Войти в полноэкранный режим Выход из полноэкранного режима

Только что отчеканенный токен имеет идентификатор токена 1.

Добавление задачи для проверки PetShop NFT

Учитывая идентификатор токена, проверить NFT:

task("petshop-check", `Checks a ${CONTRACT_NAME} NFT`)
  .addParam("address", "The contract address")
  .addParam("tokenid", "The token ID")
  .setAction(async (taskArgs) => {
    const contract = await loadNFTContract(CONTRACT_NAME, taskArgs.address);
    console.log(`Verifying token URI and owner of token #${taskArgs.tokenid}...`);
    const tokenURI = await contract.tokenURI(taskArgs.tokenid);
    const owner = await contract.ownerOf(taskArgs.tokenid);
    console.log(`  tokenURI = ${tokenURI}`);
    console.log(`  owner    = ${owner}`);
  });
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь давайте запустим эту задачу для проверки NFT, который мы только что отчеканили:

$ npx hardhat petshop-check --network localhost 
    --address 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707 
    --tokenid 1
Loaded NFT contract PetShop from 0x5FC8d32690cc91D4c39d9d3abcBD16989F875707: Pet Shop (PET)
Verifying token URI and owner of token #1...
  tokenURI = https://petshop.example/nft/foo/
  owner    = 0x70997970C51812dc3A010C7d01b50e0d17dc79C8
Войти в полноэкранный режим Выйти из полноэкранного режима

Запуск в тестовой сети Goerli

Теперь у нас все готово. Давайте запустим на тестовой сети Goerli.

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

$ npx hardhat petshop-deploy --network goerli
Deployer: 0xCc4c8184CC4A5A03babC13D832cEE3E41bE92d08 (balance: 735988912252889953)
Deployed PetShop at: 0xff27228e6871eaB08CD0a14C8098191279040c13
Querying NFT: name = Pet Shop; symbol = PET
Войти в полноэкранный режим Выход из полноэкранного режима

Монтируйте токен на мою учетную запись Джейсона:

$ npx hardhat petshop-mint --network goerli 
    --address 0xff27228e6871eaB08CD0a14C8098191279040c13 
    --to      0xCc4c8184CC4A5A03babC13D832cEE3E41bE92d08 
    --uri     https://petshop.example/nft/foo
Loaded contract from 0xff27228e6871eaB08CD0a14C8098191279040c13: Pet Shop (PET)
  * Sending tx...
  * Waiting tx to be mined...
  * Tx executed, gas used: 123193
Looking for Transfer event from receipt...
  event   = Transfer
  from    = 0x0000000000000000000000000000000000000000
  to      = 0xCc4c8184CC4A5A03babC13D832cEE3E41bE92d08
  tokenID = 1
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь давайте импортируем этот NFT в MetaMask. Для этого нам понадобится адрес контракта (0xff27228e6871eaB08CD0a14C8098191279040c13) и ID токена (1).

После импорта мы можем просмотреть его в MetaMask!

Заключение

Это мой второй день работы с Ethereum. Полный исходный код можно найти здесь: https://github.com/zhengzhong/petshop/releases/tag/day02.

Ссылки

  • Создание вашего первого смарт-контракта NFT из OpenSea

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