ERC-721: mint и ownerOf
Как создать NFT через mint, дождаться транзакции и проверить владельца tokenId через ownerOf в ethers v6.
ERC-721 хранит владельца каждого tokenId
ERC-721 — стандарт для NFT, где каждый токен имеет уникальный tokenId.
mint(to) создаёт новый NFT и записывает владельца. ownerOf(tokenId) читает, какой адрес владеет конкретным токеном.
Минтим NFT и читаем ownerOf
Создайте проект:
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:
import { defineConfig } from "hardhat/config";
export default defineConfig({
solidity: {
version: "0.8.20",
},
});Создайте файл contracts/DemoNFT.sol:
// 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:
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 в отдельном терминале:
npx hardhat nodeВо втором терминале выполните скрипт:
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 токена не существует:
owners[1] = 0x0000000000000000000000000000000000000000После mint контракт записывает владельца:
owners[1] = recipient
balances[recipient] += 1Это изменение storage, поэтому mint требует signer, gas и подтверждения в блоке.
Чем NFT отличается от ERC-20
В ERC-20 баланс — это количество взаимозаменяемых единиц:
balanceOf(user) = 100 USDCВ ERC-721 каждый токен отдельный:
ownerOf(1) = 0x...
ownerOf(2) = 0x...
ownerOf(3) = 0x...Поэтому для NFT важен не только баланс адреса, но и конкретный tokenId.
Частые ошибки
Вызывать ownerOf для несуществующего tokenId → ERC-721 должен сделать revert, если токен ещё не был создан.
Думать, что mint бесплатный read-вызов → mint меняет storage, поэтому это транзакция с gas.
Сравнивать адреса как обычные строки без нормализации → checksum может отличаться регистром. Для простой проверки используйте toLowerCase() с обеих сторон.
Что дальше
- ERC-721: tokenURI и metadata — связать tokenId с JSON-метаданными NFT
- ERC-721: setApprovalForAll — разрешить маркетплейсу управлять NFT пользователя
- Events и логи — понять событие
Transfer, которое индексируют NFT-интерфейсы