китаев.tech

ERC-721: mint и ownerOf

Как создать NFT через mint, дождаться транзакции и проверить владельца tokenId через ownerOf в ethers v6.

ERC-721 хранит владельца каждого tokenId

ERC-721 — стандарт для NFT, где каждый токен имеет уникальный tokenId.

mint(to) создаёт новый NFT и записывает владельца. ownerOf(tokenId) читает, какой адрес владеет конкретным токеном.


Минтим NFT и читаем ownerOf

Создайте проект:

bash
mkdir erc721-mint-example
cd erc721-mint-example

npm init -y
npm pkg set type="module"
npm install ethers solc tsx
npm install --save-dev hardhat typescript @types/node
mkdir contracts scripts

Создайте файл hardhat.config.ts:

typescript
import { defineConfig } from "hardhat/config";

export default defineConfig({
  solidity: {
    version: "0.8.20",
  },
});

Создайте файл contracts/DemoNFT.sol:

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

contract DemoNFT {
    string public name = "Demo NFT";
    string public symbol = "DNFT";
    uint256 public nextTokenId = 1;

    mapping(uint256 => address) private owners;
    mapping(address => uint256) private balances;

    event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);

    function mint(address to) external returns (uint256 tokenId) {
        require(to != address(0), "mint to zero address");

        tokenId = nextTokenId;
        nextTokenId += 1;

        owners[tokenId] = to;
        balances[to] += 1;

        emit Transfer(address(0), to, tokenId);
    }

    function ownerOf(uint256 tokenId) external view returns (address) {
        address owner = owners[tokenId];
        require(owner != address(0), "token does not exist");

        return owner;
    }

    function balanceOf(address owner) external view returns (uint256) {
        require(owner != address(0), "balance query for zero address");

        return balances[owner];
    }
}

Создайте файл scripts/mint-nft.ts:

typescript
import { readFileSync } from "node:fs";
import { ethers } from "ethers";
import solc from "solc";

const source = readFileSync("contracts/DemoNFT.sol", "utf8");

const input = {
  language: "Solidity",
  sources: {
    "DemoNFT.sol": {
      content: source,
    },
  },
  settings: {
    outputSelection: {
      "*": {
        "*": ["abi", "evm.bytecode.object"],
      },
    },
  },
};

const output = JSON.parse(solc.compile(JSON.stringify(input)));
const contract = output.contracts["DemoNFT.sol"].DemoNFT;

const abi = contract.abi;
const bytecode = `0x${contract.evm.bytecode.object}`;

const provider = new ethers.JsonRpcProvider("http://127.0.0.1:8545");

// Первый тестовый аккаунт Hardhat. Не используйте этот ключ в mainnet.
const minter = new ethers.Wallet(
  "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80",
  provider,
);

// Второй тестовый аккаунт Hardhat. Он получит NFT.
const recipient = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8";

const factory = new ethers.ContractFactory(abi, bytecode, minter);
const nft = await factory.deploy();

await nft.waitForDeployment();

const nftAddress = await nft.getAddress();
const tokenId = await nft.nextTokenId();

console.log("NFT address:", nftAddress);
console.log("Token ID to mint:", tokenId.toString());
console.log("Recipient before:", (await nft.balanceOf(recipient)).toString());

const tx = await nft.mint(recipient);

console.log("Mint hash:", tx.hash);

const receipt = await tx.wait();

if (!receipt || receipt.status !== 1) {
  throw new Error("Транзакция mint() не была успешно выполнена");
}

const owner = await nft.ownerOf(tokenId);
const recipientBalance = await nft.balanceOf(recipient);

console.log("Block:", receipt.blockNumber);
console.log("Gas used:", receipt.gasUsed.toString());
console.log("Owner of token:", owner);
console.log("Recipient after:", recipientBalance.toString());
console.log("Owner matches recipient:", owner.toLowerCase() === recipient.toLowerCase());

Запустите локальную сеть Hardhat в отдельном терминале:

bash
npx hardhat node

Во втором терминале выполните скрипт:

bash
npx tsx scripts/mint-nft.ts

Скрипт задеплоит NFT-контракт, создаст токен #1 для второго аккаунта и проверит владельца через ownerOf(1).


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

nextTokenId — счётчик следующего NFT. В примере первый токен получает tokenId = 1.

mint(recipient) — write-вызов. Он создаёт новый tokenId, записывает владельца и эмитит Transfer(address(0), recipient, tokenId).

tx.wait() — ждёт, пока mint-транзакция попадёт в блок.

ownerOf(tokenId) — read-вызов. Он возвращает адрес владельца конкретного NFT.

balanceOf(recipient) — показывает, сколько NFT этого контракта есть у адреса.

owner.toLowerCase() === recipient.toLowerCase() — сравнивает адреса без ошибки из-за регистра checksum-адреса.


Почему mint — это транзакция

До mint токена не существует:

text
owners[1] = 0x0000000000000000000000000000000000000000

После mint контракт записывает владельца:

text
owners[1] = recipient
balances[recipient] += 1

Это изменение storage, поэтому mint требует signer, gas и подтверждения в блоке.


Чем NFT отличается от ERC-20

В ERC-20 баланс — это количество взаимозаменяемых единиц:

text
balanceOf(user) = 100 USDC

В ERC-721 каждый токен отдельный:

text
ownerOf(1) = 0x...
ownerOf(2) = 0x...
ownerOf(3) = 0x...

Поэтому для NFT важен не только баланс адреса, но и конкретный tokenId.


Частые ошибки

Вызывать ownerOf для несуществующего tokenId → ERC-721 должен сделать revert, если токен ещё не был создан.

Думать, что mint бесплатный read-вызов → mint меняет storage, поэтому это транзакция с gas.

Сравнивать адреса как обычные строки без нормализации → checksum может отличаться регистром. Для простой проверки используйте toLowerCase() с обеих сторон.


Что дальше

Материалы китаev.tech публикуются в образовательных целях и не являются инвестиционной рекомендацией. Примеры кода и описания протоколов — для обучения; использование в продакшне на ваш собственный риск. Дисклеймер и политика конфиденциальности.