// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
interface IERC20 {
function totalSupply() external view returns (uint);
function balanceOf(address account) external view returns (uint);
function transfer(address to, uint amount) external returns (bool);
function allowance(address owner, address spender) external returns (uint);
function approve(address spender, uint amount) external returns (bool);
function transferFrom(
address spender,
address to,
uint amount
) external returns (bool);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./IPERC20.sol";
// 0x37d4fD7Bfc602e59FE9f96D6a0649D13c95294ad
contract PERC20 is IERC20 {
// 토큰의 이름
string public name;
// 토큰의 단위
string public symbol;
// 토큰의 소수점 자리 18 자리로 구성
uint8 public decimals = 18;
// 토큰의 현재 총 발행량
uint public override totalSupply;
address private owner;
// CA 주소로 이더가 전송이 되었을 때 실행시키고 싶은 동작이 있어
// 익명함수
// receive (익명함수) 특별한 함수
// CA에 이더를 받으면 자동으로 실해되는 메서드
// 이더를 CA에 전송 받았을 때 동작을 추가 할 수 있다.
receive() external payable {
// 이더를 CA가 받았을 때 실행되는 동자
// 배포자가 토큰의 발행량을 관리하고
// 다른 이용자들이 토큰을 가지고 싶으면
// 컨트랙트 배포자가 정한 비율에 따라 토큰을 가져갈 수 있게
// 소유권을 줄 토큰의 양
// 받은 이더 비율로 1이더*200개의 토큰
uint amount = msg.value * 200;
require(balance[owner] >= amount);
balance[owner] -= amount;
balance[msg.sender] += amount;
// 만약에 토큰을 다 소유권을 넘겨서 배포자가 들오있는 토큰이 없다.
// 만약 배포가 이더를 보냈다면 토큰을 발행 할 수 있게
if (msg.sender == owner) {
mint(amount);
}
}
// 컨트랙트 생성자
constructor(string memory _name, string memory _symbol, uint256 _amount) {
owner = msg.sender;
name = _name;
_symbol = symbol;
mint(_amount * (10 ** uint256(decimals)));
}
mapping(address => uint) public balance;
mapping(address => mapping(address => uint)) public override allowance;
function mint(uint amount) internal {
balance[msg.sender] += amount;
totalSupply += amount;
}
function balanceOf(address account) external view override returns (uint) {
return balance[account];
}
function transfer(
address to,
uint amount
) external override returns (bool) {
balance[msg.sender] -= amount;
balance[to] += amount;
return true;
}
function approve(
address spender,
uint amount
) external override returns (bool) {
allowance[msg.sender][spender] = amount;
return true;
}
function transferFrom(
address sender,
address to,
uint amount
) external override returns (bool) {
require(allowance[sender][msg.sender] >= amount);
allowance[sender][msg.sender] -= amount;
balance[sender] -= amount;
balance[to] += amount;
return true;
}
function burn(uint amount) external {
balance[msg.sender] -= amount;
totalSupply -= amount;
}
}
receive() external payable
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./PERC20.sol";
contract Poketmon is PERC20 {
constructor() PERC20("Poketmon", "PKT", 10000) {}
// 포켓몬 객체를 만들 것
// 이 객체 하나가 포켓몬의 데이터
struct Pokets {
string url;
string name;
}
// 포켓몬 빵 구매한 사람들의 주소를 담아놓을 것
struct Users {
address account;
}
// ERC20 토큰을 지불해서 포켓몬 빵을 하나 사는것
// 빵하나에 얼마?
// 단위 하나를 이더로 지정 10**18 소수점 단위
// 가격이 1000토큰
uint256 private tokenPrice = 1000 ether;
// 우리가 포켓몬 빵을 사면 랜덤란 스티커가 들어있는데
// 배열에 나올 수있는 포켓몬의 이름을 선언 해두자
// 한글을 사용하려면 유니코드 작성해야함... 영어로 쓰자
string[] poketmonName = ["pikachu", "kobuk", "Charmander"];
// 포켓몬 이쁜 이미지를 담아놓을 배열
string[] poketmonUrl = [
"https://archivenew.vop.co.kr/images/4869584989ba805ed7e364eefc3a4664/2011-02/11053927_Untitled-16.jpg",
"https://media.bunjang.co.kr/product/208341180_1_1691548729_w180.jpg",
"https://i1.ruliweb.com/img/19/11/08/16e49e9f1ff509386.jpg"
];
// 구매하면 한개를 얻는데
// 또 사면 두개
mapping(address => Pokets[]) public poketmons;
// 한 번이라도 포켓몬 빵을 구매한 사람들의 주소를 가지고 있는 Users 객체
Users[] public users;
// 지갑 주소가 가지고있는 포켓몬 조회
function getPoketmon() public view returns (Pokets[] memory) {
return poketmons[msg.sender];
}
function getPoketmonUsers() public view returns (Users[] memory) {
return users;
}
// 포켓몬 거래 함수
function tradePoketmon(
address to,
string memory tradePoketmonName
) public returns (bool) {
Pokets[] memory tempPoketArr = poketmons[msg.sender];
string memory tradePoketmon;
for (uint256 index = 0; index < tempPoketArr.length; index++) {
if (
keccak256(abi.encodePacked(tempPoketArr[index].name)) ==
keccak256(abi.encodePacked(tradePoketmonName))
) {
string memory name = poketmons[msg.sender][index].name;
string memory url = poketmons[msg.sender][index].url;
poketmons[to].push(Pokets(url, name));
if (index < poketmons[msg.sender].length - 1) {
poketmons[msg.sender][index] = poketmons[msg.sender][
tempPoketArr.length - 1
];
}
poketmons[msg.sender].pop();
// 로직
}
}
return true;
}
// function getUsersWhoHaveDrawn() public view returns(Users[] memory){
// Users[] memory tempUser;
// for(uint i=0;i<users.length;i++){
// if(users[i].length !=0){
// tempUser.push(users[i]);
// }
// }
// return tempUser;
// }
function buyPoketmon() public {
require(balance[msg.sender] >= tokenPrice);
balance[msg.sender] -= tokenPrice;
totalSupply -= tokenPrice;
uint random = uint(
keccak256(
abi.encodePacked(block.timestamp, block.coinbase, block.number)
)
);
random = uint(random % 3); //0~3까지의 랜덤값
// Pokets구조체 형태로 객체를 만들어서 배열에 푸쉬
poketmons[msg.sender].push(
Pokets(poketmonUrl[random], poketmonName[random])
);
// 유저가 포켓몬 빵을 한 번 산적이 있는지
bool isUser = false;
for (uint256 i = 0; i < users.length; i++) {
if (users[i].account == msg.sender) {
isUser = true;
break;
}
}
if (!isUser) {
users.push(Users(msg.sender));
}
}
}
Pokets은 포켓몬의 정보가 들어있는 구조체이고, Users는 포켓몬을 한 번이라도 구매한 유저가 있는 구조체 이다.
tokenPrice 는 토큰의 단위이다. 10000개를 발행하면 mint함수에서 10^18을 곱한 값 (wei)로 저장되는데, 이는 ether와 단위가 같아 1000 ether이면 1000 * 10^18이 된다. 즉, 1000ether는 1000토큰이 되는 것이다.
tradePoketmon에서는 해당 계정의 포켓몬을 순회 탐색하여 사용자가 원하는 포켓몬이 나올 경우, 이를 to 계정에 보내고 기존 계정에 있던 포켓몬(poketmons[msg.sender])은 삭제된다.
이 때 구조체에 있는 string은 일반적인 방법으로 비교가 안된다.
keccak256(abi.encodePacked(tempPoketArr[index].name)) ==
keccak256(abi.encodePacked(tradePoketmonName))
if (index < poketmons[msg.sender].length - 1) {
poketmons[msg.sender][index] = poketmons[msg.sender][ tempPoketArr.length - 1 ];
}
import React, { useEffect, useState } from "react";
import useWeb3 from "./hooks/web3.hook";
import abi from "./abi/Poketmon.json";
function App() {
const { user, web3 } = useWeb3();
const [contract, setContract] = useState(null);
const { token, setToken } = useState(0);
const [account, setAccount] = useState([]);
const [trainer, setTrainer] = useState([]);
const [selectedPoketmon, setSelectedPoketmon] = useState();
const [toAccount, setToAccount] = useState("");
const img = {
width: "200px",
height: "200px",
};
useEffect(() => {
if (web3 !== null) {
if (contract) {
return;
}
const poketmon = new web3.eth.Contract(
abi,
"0x6c4496dC9196A8458875a7eD26229FfF41EF6FAb",
{ data: "" }
);
setContract(poketmon);
}
}, [web3]);
// 해당 지갑의 포켓몬 조회
const getPoketmon = async (account) => {
const result = await contract.methods.getPoketmon().call({ from: account });
console.log(result);
return result;
};
// 지갑의 토큰 양 조회
const getToken = async (account) => {
if (!contract) {
return;
}
let result = web3.utils
.toBigInt(await contract.methods.balanceOf(account).call())
.toString(10);
result = await web3.utils.fromWei(result, "ether");
return result;
};
// 메타 마스크 계정들 조회
const getAccount = async () => {
const accounts = await window.ethereum.request({
method: "eth_requestAccounts",
});
const _accounts = await Promise.all(
accounts.map(async (account) => {
const token = await getToken(account);
const poketmon = await getPoketmon(account);
// 추가로 어떤 포켓몬도 있는지 추가할 부분
return { account, token, poketmon };
})
);
setAccount(_accounts);
};
const getNewPoketmon = async () => {
await contract.methods.buyPoketmon().send({
from: user.account,
});
};
const getUsersWhoHaveDrawn = async () => {
const result = await contract.methods.getPoketmonUsers().call({
from: user.account,
});
console.log(result);
setTrainer(result);
};
// 포켓몬 트레이드 시스템
const tradePoketmon = async () => {
await contract.methods.tradePoketmon(toAccount, selectedPoketmon).send({
from: user.account,
});
};
// 1.포켓몬 랜덤으로 뽑을 수 있는 버튼
// 2. 한 번이라도 뽑은 계정만 모으고 어떤 포켓몬을 가지고 있는지 보여주기
// 3.포켓몬 거래(소유권) 함수 작성
// 4.포켓몬 대전 판돈(선택)
useEffect(() => {
if (!contract) {
return;
}
getAccount();
}, [contract]);
useEffect(() => {
console.log(selectedPoketmon);
}, [selectedPoketmon]);
if (user.account === null) {
return "지갑 연결하세요";
}
return (
<div>
<div>토큰 보유량:{token}</div>
{account.map((el, index) => {
return (
<div key={index}>
계정:{el.account} <br />
토큰 값:{el.token}
<div> 포켓몬들</div>
<div style={{ display: "flex" }}>
{el.poketmon.map((el2, index2) => {
return (
<div key={index2}>
{el2.name}: <img style={img} src={el2.url} alt="포켓몬" />
</div>
);
})}
</div>
</div>
);
})}
<div style={{ width: "100%", height: "500px", margin: "auto" }}>
<img
src="https://mblogthumb-phinf.pstatic.net/20160326_233/poi8969_1458975983221ymkBv_JPEG/cafe_naver_com_20160326_152644.jpg?type=w2"
alt="오박사"
/>{" "}
<br />
<button onClick={getNewPoketmon}>너의 포켓몬을 골라보려어어엄!</button>
</div>
<div>
<h2>포켓몬 트레이너들 보기</h2>
<button onClick={getUsersWhoHaveDrawn}>
스바라시한 트레이너들 보기
</button>
{trainer &&
trainer.map((el, index) => {
return (
<ol key={index}>
<li>{el.account}</li>
</ol>
);
})}
</div>
<div style={{ width: "100%", height: "400px", margin: "auto" }}>
<h2>포켓몬 입양 보내기</h2>
<div>
당신이 세상에서 전부인 불쌍한 포켓몬을 입양보내는 시스템 입니다.
</div>
<div>입양 보낼(ㅠㅠ) 포켓몬 선택하기</div>
<label>
피카츄
<input
name="radio"
type="radio"
value={"pikachu"}
onChange={(e) => {
setSelectedPoketmon(e.target.value);
}}
/>
</label>
<label>
파이리
<input
name="radio"
type="radio"
value={"Charmander"}
onChange={(e) => {
setSelectedPoketmon(e.target.value);
}}
/>
</label>
<label>
꼬북칩
<input
name="radio"
type="radio"
value={"kobuk"}
onChange={(e) => {
setSelectedPoketmon(e.target.value);
}}
/>
</label>
<br />
<br />
<br />
<label>당신보다 훌륭한 주인의 wallet입력하기</label>
<input
onChange={(e) => {
setToAccount(e.target.value);
}}
/>
<button onClick={tradePoketmon}>입양 ㄱㄱ</button>
</div>
</div>
);
}
export default App;
실제 토큰으로 nft 비스무리한 것을 교환하니 굉장히 재미가 있었다.
기본적으로 수수료 없이 원하는 토큰을 교환하고 이를 실시간으로 확인 한다는점이 의의가 있었고, 코어쪽 (IERC20)등등을 더 공부하여 베이스를 더 이해 할 수 있도록 해야겠다.