Lazy Minting Part2
Lazy Minting(Part2)
Signature
Signature : wallet만을 이용해서 특정 EIP712에 해당하는 서명을 도출해냄
- 온체인(blockchain / on-chain)과 상호작용이 없이 순전히 오프체인(off-chain)을 이용하여 만듬
 - Signature 생성시 domain에 보증할 smart contract 정보(domain), voucher의 실제 값(voucher), voucher의 구조체(types)를 사용
 - 만들어진 서명(signature)은 온체인(on-chain)이 아닌 오프체인(off-chain)인 데이터베이스(database)에 저장함
 
Domain
Domain : 서명이 유효한 contract 및 제반 사항을 확인함
- name : smart contract의 EIP712 constructor name
 - version : smart contract의 EIP712 constructor version
 - chainId : wallet이 최근 선택한 network chain의 고유 id
 - vertifyContract : smart contract의 contract address
 
Wallet address & Contract address -> Signature(off-chain)
wallet을 이용하여 signature 생성
- 만들어진 서명(signature)은 데이터베이스(database)에 저장함
 - contract address + user wallet address -> Keccak256 Hash -> 332 entry Uint8Array -> Signature
 
Javascript(off-chain)
import Web3 from 'web3';
import { BigNumber, ethers} from 'ethers'
const SIGNING_DOMAIN_NAME = "EIP712"
const SIGNING_DOMAIN_VERSION = "1"
async createVoucher(tokenId, uri, minPrice) {
  const provider = new ethers.providers.Web3Provider(window.ethereum)
  const signer = provider.getSigner()
  console.log(signer)
  const voucher = { tokenId, uri, minPrice }
  const domain = await this._signingDomain()
  const types = {
    NFTVoucher: [
      {name: "tokenId", type: "uint256"},
      {name: "minPrice", type: "uint256"},
      {name: "uri", type: "string"},  
    ]
  }
  const signature = await signer._signTypedData(domain, types, voucher)
  return {
    ...voucher,
    signature,
  }
},
async _signingDomain() {
  if (this.domain != null) {
  	return this.domain
  }
  const chainId = await web3.eth.getChainId()
  console.log(typeof(chainId))
  this.domain = {
  	name: SIGNING_DOMAIN_NAME,
  	version: SIGNING_DOMAIN_VERSION,
  	chainId: BigNumber.from(chainId),
  	verifyingContract: this.nftContractAddress
  }
  return this.domain
}
Signature -> Wallet address(on-chain)
smart contract를 이용하여 address 도출
- ECDSA.sol openzeppelin library 사용
 - smart contract(on-chain)을 이용하여 signature로 wallet address 도출
 - signature -> Construct Keccak256 Hash -> toEthSignedMessageHash -> Recover the signer’s wallet address(public key)
 
_hash(voucher)
- _hashTypedDataV4를 이용하여 voucher parameter를 EIP712 메세지의 인코딩된 해시를 반환
 
verify(voucher, signature)
- ECDSA.recover(_hash(voucer), signature)를 통해서 signer의 wallet address를 도출해냄
 
redeem(redeemer, voucher)
- verify 함수와 voucher parameter를 이용하여 signer의 address를 도출함
 - 도출된 signer의 address와 voucher의 nft 정보를 이용해서 mint함
 - mint 후 transfer(redeemer, voucher.tokenId)를 이용하여 nft 소유권을 구매자에게 양도
 
민팅 & 구매 시 : minting transaction + transfer transaction 두 번 발생(가스비 2번 발생)
레이지민팅 & 구매 : minting, transfer transaction 한 번 발생(가스비 1번 발생)
가스비 : 레이지민팅 « 민팅
const redeemer = (await this.getEthWallet())[0];
const mynftContractAddress = new web3.eth.Contract(nftContract.abi, nftContractAddress);
const signer = '0x0000000000000000000000000000000000000000'
const nonce = await web3.eth.getTransactionCount(
  publicAddress,
  "latest"
); 
const tx = {
  from: redeemer,
  to: nftContractAddress,
  nonce: nonce,
  value: ethers.utils.parseEther(price.toString()).toHexString(),
  data: mynftContractAddress.methods.redeem(redeemer, voucher).encodeABI(),
};
Smart Contract(on-chain)
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.7;
pragma experimental ABIEncoderV2;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol";
contract nftToken is ERC721URIStorage, EIP712, AccessControl {
  using ECDSA for bytes32;
  bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
  mapping (address => uint256) pendingWithdrawals;
  constructor(address payable minter)
    ERC721("erc721", "my-nft") 
    EIP712("EIP712", "1") {
      _setupRole(MINTER_ROLE, minter);
    }
  struct NFTVoucher {
    uint256 tokenId;
    uint256 minPrice;
    string uri;
    bytes signature;
  }
  function redeem(address redeemer, NFTVoucher calldata voucher) public payable returns (uint256) {
    address signer = _verify(voucher);
    require(hasRole(MINTER_ROLE, signer), "Signature invalid or unauthorized");
    require(msg.value >= voucher.minPrice, "Insufficient funds to redeem");
    _mint(signer, voucher.tokenId);
    _setTokenURI(voucher.tokenId, voucher.uri);
    _transfer(signer, redeemer, voucher.tokenId);
    address payable tokenOwner = payable(signer);
    tokenOwner.transfer(msg.value);       
    return voucher.tokenId;
  }
  function _hash(NFTVoucher calldata voucher) internal view returns (bytes32) {
    return _hashTypedDataV4(keccak256(abi.encode(
      keccak256("NFTVoucher(uint256 tokenId,uint256 minPrice,string uri)"),
      voucher.tokenId,
      voucher.minPrice,
      keccak256(bytes(voucher.uri))
    )));
  }
  function _verify(NFTVoucher calldata voucher, bytes memory signature) internal view returns (address) {
    bytes32 digest = _hash(voucher);
    return ECDSA.recover(digest, signature);
  }
  function supportsInterface(bytes4 interfaceId) public view virtual override (AccessControl, ERC721) returns (bool) {
    return ERC721.supportsInterface(interfaceId) || AccessControl.supportsInterface(interfaceId);
  }
}