PetShop 프로젝트, 2일차: ERC721 표준 PetShop NFT 생성

둘째 날에는 OpenZeppelin을 사용하여 간단한 ERC721 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 업그레이드 플러그인을 사용하여 ethersupgrades 인스턴스에 직접 액세스할 수 있습니다.
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

참조


  • Creating your first NFT smart contract from OpenSea
  • 좋은 웹페이지 즐겨찾기