PetShop 프로젝트, 2일차: ERC721 표준 PetShop NFT 생성
OpenZeppelin 설치
upgradeable variant of the OpenZeppelin Contracts library을 설치해야 Solidity에서 ERC721 호환 NTF를 쉽게 생성할 수 있습니다.
또한 OpenZeppelin Upgrades plugin for 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
를 생성하고 ERC721 standard을 준수하는 PetShop NFT에 대한 몇 가지 테스트를 추가합니다. 추가한 mintToken()
메서드 외에도 tokenURI()
, ownerOf()
및 balanceOf()
와 같은 일부 ERC721 메서드도 테스트합니다.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의 태스크는 구성 및 매개변수를 노출하는 Hardhat Runtime Environment에 대한 액세스 권한을 얻는 비동기식 JavaScript 기능은 물론 삽입되었을 수 있는 다른 태스크 및 모든 플러그인 개체에 대한 프로그래밍 방식 액세스입니다.
Hardhat Runtime Environment는 전역 범위에서 사용할 수 있습니다. Hardhat의 ether.js 플러그인(Hardhat Toolbox에 포함됨)과 OpenZeppelin 업그레이드 플러그인을 사용하여
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 Network 데몬 노드를 시작합니다.
$ 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,
}
이름과 주소만으로 계약을 로드하는 방법에 유의하십시오. hardhat-ethers plugin에 의해
getContractAt()
개체에 추가된 ethers
도우미 메서드를 사용합니다.tasks/petshop.js
로 돌아가기 . PetShop NFT를 발행하고 계정에 수여하는 방법: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
방금 발행한 토큰의 토큰 ID는
1
입니다.PetShop NFT를 확인하는 작업 추가
토큰 ID가 주어지면 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
내 계정 Jason에 토큰 발행:
$ 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
참조
Reference
이 문제에 관하여(PetShop 프로젝트, 2일차: ERC721 표준 PetShop NFT 생성), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://dev.to/zhengzhong/the-petshop-project-second-day-on-ethereum-578a텍스트를 자유롭게 공유하거나 복사할 수 있습니다.하지만 이 문서의 URL은 참조 URL로 남겨 두십시오.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)