728x90
목차
- 디렉토리 구조
- 모듈 설치 및 기본 설정
- 구현 코드
- 결과
디렉토리 구조
모듈 설치 및 기본 설정
프론트
npx create-next-app@latest --typescript front
npm i @chakra-ui/react @emotion/react@^11 @emotion/styled@^11 framer-motion@^6
npm install web3
샤크라라는 라이브러리를 사용해서
이미 만들어져있는 리액트 컴포넌트를 사용해서 편하게 작업할 것임
백
mkdir truffle
cd truffle
truffle init
cd contracts
npm init
npm i openzeppelin-solidity
truffle.config.js에서 development 주석 해제
nft 토큰마다 랜덤으로 들어갈 고유한 사진들을 json 파일로 만들어놓은 메타데이터이다.
오늘은 이걸 사용하겠다.(사진이 나오진 않음)
구현
백
truffle/contracts/gyulToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "./node_modules/openzeppelin-solidity/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "./node_modules/openzeppelin-solidity/contracts/access/Ownable.sol";
import "./node_modules/openzeppelin-solidity/contracts/utils/Strings.sol";
contract GyulToken is ERC721Enumerable, Ownable{
uint constant public MAX_TOKEN_COUNT = 1000; //발행량 설정
uint public mint_price = 1 ether; // 토큰 하나 민팅하려면 1이더 필요
string public metadataURI;
constructor(string memory _name, string memory _symbol, string memory _metadataURI) ERC721(_name, _symbol){
metadataURI = _metadataURI;
}
struct TokenData {
uint Rank;
uint Type;
}
mapping(uint => TokenData) public TokenDatas;
//TokenDatas란 녀석은 uint 값(숫자)을 넣어주면 TokenData 형식의 구조체가 나오는 애다.
uint[4][4] public tokenCount;
// 4 * 4 이중배열 형태
function mintToken() public payable{
require(msg.value == mint_price);
// 1000 0~999
require(MAX_TOKEN_COUNT > totalSupply());
// 전체 토큰 개수는 처음에지정한 토큰 발행 총량을 넘을 수 없음
uint tokenId = totalSupply() + 1;
// 토큰 id 부여
TokenDatas[tokenId] = getRandomNum(msg.sender, tokenId);
tokenCount[TokenDatas[tokenId].Rank-1][TokenDatas[tokenId].Type-1] += 1;
payable(Ownable.owner()).transfer(msg.value);
//owner contract 배포자
_mint(msg.sender, tokenId);
}
function tokenURI(uint _tokenId) public override view returns(string memory){
string memory Rank = Strings.toString(TokenDatas[_tokenId].Rank);
string memory Type = Strings.toString(TokenDatas[_tokenId].Type);
return string(abi.encodePacked(metadataURI, "/",Rank,"/",Type,".json"));
//tokenId -> rank, type 1 -> (1,4) 2 -> (2,1)
// url return... baseurl http://localhost:3000
// return http://localhost:3000/metadata/1/4.json
}
function getRandomNum(address _owner, uint _tokenId) private pure returns(TokenData memory){
uint randomNum = uint(keccak256(abi.encodePacked(_owner, _tokenId)))%100; //32byte
TokenData memory data;
if (randomNum < 5) {
if (randomNum == 1) {
data.Rank = 4;
data.Type = 1;
} else if (randomNum == 2) {
data.Rank = 4;
data.Type = 2;
} else if (randomNum == 3) {
data.Rank = 4;
data.Type = 3;
} else {
data.Rank = 4;
data.Type = 4;
}
} else if (randomNum < 13) {
if (randomNum < 7) {
data.Rank = 3;
data.Type = 1;
} else if (randomNum < 9) {
data.Rank = 3;
data.Type = 2;
} else if (randomNum < 11) {
data.Rank = 3;
data.Type = 3;
} else {
data.Rank = 3;
data.Type = 4;
}
} else if (randomNum < 37) {
if (randomNum < 19) {
data.Rank = 2;
data.Type = 1;
} else if (randomNum < 25) {
data.Rank = 2;
data.Type = 2;
} else if (randomNum < 31) {
data.Rank = 2;
data.Type = 3;
} else {
data.Rank = 2;
data.Type = 4;
}
} else {
if (randomNum < 52) {
data.Rank = 1;
data.Type = 1;
} else if (randomNum < 68) {
data.Rank = 1;
data.Type = 2;
} else if (randomNum < 84) {
data.Rank = 1;
data.Type = 3;
} else {
data.Rank = 1;
data.Type = 4;
}
}
return data;
}
function setMetadataURI(string memory _uri) public onlyOwner{
metadataURI = _uri;
}
function getTokenRank(uint _tokenId) public view returns(uint){
return TokenDatas[_tokenId].Rank;
}
function getTokenType(uint _tokenId) public view returns(uint){
return TokenDatas[_tokenId].Type;
}
function getTokenCount() public view returns(uint[4][4] memory){
return tokenCount;
}
}
truffle/contracts/SaleToken.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
import "./gyulToken.sol";
contract SaleToken{
GyulToken public Token;
constructor(address _tokenAddress){
Token = GyulToken(_tokenAddress);
}
struct TokenInfo{
uint tokenId;
uint Rank;
uint Type;
uint price;
}
// 토큰 가격 맵핑
mapping(uint => uint) public tokenPrices;
// 판매 토큰 배열
uint[] public SaleTokenList;
function SalesToken(uint _tokenId, uint _price) public {
address tokenOwner = Token.ownerOf(_tokenId);
require((tokenOwner) == msg.sender);
require(_price > 0);
// 오픈씨 isapprovedForall
require(Token.isApprovedForAll(msg.sender, address(this)));
tokenPrices[_tokenId] = _price;
SaleTokenList.push(_tokenId);
}
//구매 구매자 -> CA-> 판매자
function PurchaseToken(uint _tokenId) public payable{
address tokenOwner = Token.ownerOf(_tokenId);
require(tokenOwner != msg.sender);
require(tokenPrices[_tokenId] > 0);
// 토큰 가격은 0보다 커야된다. 그래야 팔지...
require(tokenPrices[_tokenId]<= msg.value);
payable(tokenOwner).transfer(msg.value);
Token.transferFrom(tokenOwner, msg.sender, _tokenId);
tokenPrices[_tokenId] = 0;
popSaleToken(_tokenId);
}
//판매 취소
function cancelSaleToken(uint _tokenId) public {
address tokenOwner = Token.ownerOf(_tokenId);
require(tokenOwner == msg.sender);
require(tokenPrices[_tokenId] > 0);
tokenPrices[_tokenId] = 0;
popSaleToken(_tokenId);
}
//판매 끝났으면, 판매 리스트에서 삭제
function popSaleToken(uint _tokenId) private returns(bool){
for(uint i=0; i<SaleTokenList.length; i++){
if (SaleTokenList[i] == _tokenId){
// i = index
SaleTokenList[i] = SaleTokenList[SaleTokenList.length - 1];
SaleTokenList.pop();
return true;
}
}
return false;
}
//판매중인 토큰 리스트
function getSaleTokenList() public view returns(TokenInfo[] memory){
require(SaleTokenList.length > 0);
TokenInfo[] memory list = new TokenInfo[](SaleTokenList.length);
for(uint i = 0; i < SaleTokenList.length; i++){
uint tokenId = SaleTokenList[i];
uint Rank = Token.getTokenRank(tokenId);
uint Type = Token.getTokenType(tokenId);
uint price = tokenPrices[tokenId];
list[i] = TokenInfo(tokenId, Rank, Type, price);
}
return list;
}
//토큰의 소유자
function getOwnerTokens(address _tokenOwner) public view returns(TokenInfo[] memory){
uint balance = Token.balanceOf(_tokenOwner);
require(balance != 0);
TokenInfo[] memory list = new TokenInfo[](balance);
for (uint i=0; i<balance; i++){
uint tokenId = Token.tokenOfOwnerByIndex(_tokenOwner, i);
uint Rank = Token.getTokenRank(tokenId);
uint Type = Token.getTokenType(tokenId);
uint price = tokenPrices[tokenId];
list[i] = TokenInfo(tokenId, Rank, Type, price);
}
return list;
}
}
truffle/migrations/2_deploy_minting.js
const GyulToken = artifacts.require("GyulToken");
const SaleToken = artifacts.require("SaleToken");
module.exports = async (deployer) => {
await deployer.deploy(
GyulToken,
"test",
"gyul",
"https://gateway.pinata.cloud/ipfs/QmUsEKtVS5Gn4rZWbYfD7D4qLLKPf1YbsBWYaSqtmCPBzf"
);
const gyulTokenInstance = await GyulToken.deployed();
await deployer.deploy(SaleToken, gyulTokenInstance.address);
};
truffle migration --reset
bulid에 생성 된 GyulToken.json과 SaleToken.json 파일을 복사해서 프론트 contracts 디렉토리에 넣어주어 편하게 작업하겠다. (컴파일 할 때마다 바꿔주는 게 귀찮긴 하지만...)
프론트
front/next-env.d.ts
import { MetaMaskInpageProvider } from "@metamask/providers";
declare global {
interface Window {
ethereum?: MetaMaskInpageProvider;
}
}
front/index.d.ts
declare interface Window {
ethereum: any;
}
front/pages/_app.tsx
import type { AppProps } from "next/app";
import { ChakraProvider } from "@chakra-ui/react";
function MyApp({ Component, pageProps }: AppProps) {
return (
<ChakraProvider>
<Component {...pageProps} />
</ChakraProvider>
);
}
export default MyApp;
front/pages/index.tsx
import { Box, Button, Flex, useAccordion, useDisclosure } from "@chakra-ui/react";
import type { NextPage } from "next";
import { useEffect } from "react";
import useAccount from "../hooks/useAccount";
import useWeb3 from "../hooks/useWeb3";
import MintingModal from "./components/mintingModal";
const Home: NextPage = () => {
const { isOpen, onOpen, onClose } = useDisclosure();
const { account } = useAccount();
const { web3, gyulToken, saleToken } = useWeb3();
useEffect(() => {
console.log(account);
console.log(web3);
console.log(gyulToken);
console.log(saleToken);
});
return (
<>
<Flex bg="red.100" minH="100vh" justifyContent="center" alignItems="center">
<Button colorScheme="blue" onClick={onOpen}>
민팅하기
</Button>
</Flex>
<MintingModal isOpen={isOpen} onClose={onClose} />
</>
);
};
export default Home;
front/hooks/useWeb3.tsx
import { useEffect, useState } from "react";
import Web3 from "web3";
import { Contract } from "web3-eth-contract";
import { AbiItem } from "web3-utils";
const useWeb3 = () => {
const [web3, setWeb3] = useState<Web3 | undefined>(undefined);
const [gyulToken, setGyulToken] = useState<Contract>();
const [saleToken, setSaleToken] = useState<Contract>();
const getWeb3 = () => {
try {
if (window.ethereum) {
setWeb3(new Web3(window.ethereum as any));
}
} catch (e) {
console.error(e);
}
};
const getGyulToken = (networkId: number) => {
if (!web3) return;
const gyulTokenJSON = require("../contracts/GyulToken.json");
const abi: AbiItem = gyulTokenJSON.abi;
const ca: string = gyulTokenJSON.networks[networkId]?.address;
const instance = new web3.eth.Contract(abi, ca);
setGyulToken(instance);
};
const getSaleToken = (networkId: number) => {
if (!web3) return;
const saleTokenJSON = require("../contracts/SaleToken.json");
const abi: AbiItem = saleTokenJSON.abi;
const ca: string = saleTokenJSON.networks[networkId]?.address;
const instance = new web3.eth.Contract(abi, ca);
setSaleToken(instance);
};
useEffect(() => {
getWeb3();
}, []);
useEffect(() => {
(async () => {
if (!web3) return;
const networkId: number = await web3.eth.net.getId();
getGyulToken(networkId);
getSaleToken(networkId);
})();
}, [web3]);
return { web3, gyulToken, saleToken };
};
export default useWeb3;
front/hooks/useAccount.tsx
import { useEffect, useState } from "react";
import Web3 from "web3";
import { Contract } from "web3-eth-contract";
import { AbiItem } from "web3-utils";
const useWeb3 = () => {
const [web3, setWeb3] = useState<Web3 | undefined>(undefined);
const [gyulToken, setGyulToken] = useState<Contract>();
const [saleToken, setSaleToken] = useState<Contract>();
const getWeb3 = () => {
try {
if (window.ethereum) {
setWeb3(new Web3(window.ethereum as any));
}
} catch (e) {
console.error(e);
}
};
const getGyulToken = (networkId: number) => {
if (!web3) return;
const gyulTokenJSON = require("../contracts/GyulToken.json");
const abi: AbiItem = gyulTokenJSON.abi;
const ca: string = gyulTokenJSON.networks[networkId]?.address;
const instance = new web3.eth.Contract(abi, ca);
setGyulToken(instance);
};
const getSaleToken = (networkId: number) => {
if (!web3) return;
const saleTokenJSON = require("../contracts/SaleToken.json");
const abi: AbiItem = saleTokenJSON.abi;
const ca: string = saleTokenJSON.networks[networkId]?.address;
const instance = new web3.eth.Contract(abi, ca);
setSaleToken(instance);
};
useEffect(() => {
getWeb3();
}, []);
useEffect(() => {
(async () => {
if (!web3) return;
const networkId: number = await web3.eth.net.getId();
getGyulToken(networkId);
getSaleToken(networkId);
})();
}, [web3]);
return { web3, gyulToken, saleToken };
};
export default useWeb3;
front/pages/components/mintingModal.tsx
import {
Button,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
Text,
} from "@chakra-ui/react";
import { FC } from "react";
import useAccount from "../../hooks/useAccount";
import useWeb3 from "../../hooks/useWeb3";
interface MintingModalProps {
isOpen: boolean;
onClose: () => void;
}
const MintingModal: FC<MintingModalProps> = ({ isOpen, onClose }) => {
const { account } = useAccount();
const { web3, gyulToken, saleToken } = useWeb3();
const handleClick = async () => {
try {
if (!account || !web3 || !gyulToken || !saleToken) return;
const response = await gyulToken.methods.mintToken().send({
from: account,
value: web3.utils.toWei("1", "ether"),
});
console.log(response);
if (response.status) {
//내가 민팅한 내역을 볼 수 있게
console.log("민팅 성공");
const tokenId: string = await gyulToken.methods.totalSupply().call();
// console.log(total);
}
} catch (e) {
console.error(e);
}
};
return (
<>
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Modal Title</ModalHeader>
<ModalCloseButton />
<ModalBody>
<Text>민팅 시 1 ETH가 소모 됩니다.</Text>
</ModalBody>
<ModalFooter>
<Button colorScheme="green" mr={3} onClick={handleClick}>
민팅하기
</Button>
<Button mr={3} colorScheme="blue" onClick={onClose}>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
</>
);
};
export default MintingModal;
결과
민팅이 잘 된 것을 확인할 수 있다.
728x90
'블록체인 > 스마트 컨트랙트' 카테고리의 다른 글
[Remix IDE] Opensea 마켓에 NFT 토큰 생성해서 올려보기 (0) | 2022.07.25 |
---|---|
[스마트 컨트랙트, open-zeppelin] 토큰 <-> 이더리움 스왑 구현해보기 (0) | 2022.07.22 |
[스마트 컨트랙트] 물건 사고, 환불하는 dApp 간단히 구현 (0) | 2022.07.20 |
[스마트 컨트랙트] 투표 dApp 만들어 보기 (0) | 2022.07.18 |
[truffle, 스마트 컨트랙트] 토큰 생성해보기 (0) | 2022.07.15 |