BlockChain
(BlockChain) NFT-Market (2)
JJeongHyun
2023. 3. 17. 20:31
반응형
https://developerjjh.tistory.com/179
이전 게시물에 앞서 코드를 이어가 보려고 한다
nft-market/back 추가 라이브러리 설치
npm i axios web3 web3-utils
mkdir contracts
back/contracts
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "./NftToken.sol";
contract SaleToken {
NftToken public Token;
constructor(address _tokenAddress) {
Token = NftToken(_tokenAddress);
}
struct TokenInfo {
uint tokenId;
uint price;
string tokenURI;
}
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);
require(Token.isApprovedForAll(msg.sender, address(this)));
tokenPrices[_tokenId] = _price;
SaleTokenList.push(_tokenId);
}
function PurchaseToken(uint _tokenId) public payable {
address tokenOwner = Token.ownerOf(_tokenId);
require(tokenOwner != msg.sender);
require(tokenPrices[_tokenId] > 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) {
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 price = tokenPrices[tokenId];
string memory tokenURI = Token.tokenURI(tokenId);
list[i] = TokenInfo(tokenId, price, tokenURI);
}
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);
string memory tokenURI = Token.tokenURI(tokenId);
uint price = tokenPrices[tokenId];
list[i] = TokenInfo(tokenId, price, tokenURI);
}
return list;
}
function getLatestToken(
address _tokenOwner
) public view returns (TokenInfo memory) {
uint balance = Token.balanceOf(_tokenOwner);
uint tokenId = Token.tokenOfOwnerByIndex(_tokenOwner, balance - 1);
uint price = tokenPrices[tokenId];
string memory tokenURI = Token.tokenURI(tokenId);
return TokenInfo(tokenId, price, tokenURI);
}
}
이 전에 사용했던 NFT 거래 컨트랙트 SaleToken.sol 파일을 가져온다
import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
import "../node_modules/@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "../node_modules/@openzeppelin/contracts/access/Ownable.sol";
import "../node_modules/@openzeppelin/contracts/utils/Counters.sol";
- SaleToken.sol을 가져와서 해당 폴더(back/contracts) 내 넣어준다
- NftToken.sol을 만들어서 ERC721Enumerable, ERC721URIStorage, Ownable을 상속
두 개의 solidity파일을 정상적으로 완성하고 remix 명령어를 실행하기 전에
react와 express환경부터 실행한다
# react
yarn start
# npm
npm run start:dev
# remix
npx remixd -s . -u https://remix.ethereum.org
remix 웹페이지에서 각각의 solidity파일에 대해 deploy을 하여 CA값을 뽑아낸다
CA 값들을 .env 파일에 저장해 둔다
src/index.ts
import Web3 from "web3";
import { AbiItem } from "web3-utils";
import axios from "axios";
import { abi as NftAbi } from "../contracts/artifacts/NftToken.json";
import { abi as SaleAbi } from "../contracts/artifacts/SaleToken.json";
const web3 = new Web3("http://ganache.test.errorcode.help:8545");
const deployed = new web3.eth.Contract(NftAbi as AbiItem[], process.env.NFT_CA);
const obj: { nonce: number; to: string; from: string; data: string } = {
nonce: 0,
to: "",
from: "",
data: "",
};
obj.nonce = await web3.eth.getTransactionCount(req.body.from);
obj.to = process.env.NFT_CA;
obj.from = req.body.from;
obj.data = deployed.methods.safeMint(jsonResult.IpfsHash).encodeABI();
res.send(obj);
- abi정보가 담겨있는 JSON파일은 remix환경에서 각각의 solidity 파일을 컴파일하면 artifacts폴더 내에 생긴다
- 넘어온 obj를 front에서 result로 응답
- 이후 해당 값으로 트랜잭션을 보내기 위해 web3을 받아온다
front/src/components/Mint.tsx
import Web3 from "web3";
export const Mint = ({ web3, account }: { web3: Web3; account: string }) => {
formData.append("from", account);
web3.eth.sendTransaction(result);
};
- Mint 컴포넌트에 account 정보를 props로 넘겨준다
back/src/index.ts
app.post(
"/api/mint",
upload.single("file"),
async (req: Request, res: Response) => {
const { name, description }: { name: string; description: string } =
req.body;
const imgResult: {
IpfsHash: string;
PinSize: number;
Timestamp: string;
isDuplicate?: boolean;
} = await pinata.pinFileToIPFS(Readable.from(req.file.buffer), {
pinataMetadata: { name: Date.now().toString() },
pinataOptions: { cidVersion: 0 },
});
if (imgResult.isDuplicate) console.log("img Duplicated");
const jsonResult = await pinata.pinJSONToIPFS(
{
name,
description,
image: `https://gateway.pinata.cloud/ipfs/${imgResult.IpfsHash}`,
},
{
pinataMetadata: { name: Date.now().toString() + ".json" },
pinataOptions: { cidVersion: 0 },
}
);
const deployed = new web3.eth.Contract(
NftAbi as AbiItem[],
process.env.NFT_CA
);
const obj: { nonce: number; to: string; from: string; data: string } = {
nonce: 0,
to: "",
from: "",
data: "",
};
obj.nonce = await web3.eth.getTransactionCount(req.body.from);
obj.to = process.env.NFT_CA;
obj.from = req.body.from;
obj.data = deployed.methods.safeMint(jsonResult.IpfsHash).encodeABI();
res.send(obj);
}
);
- multer로 업로드한 이미지 데이터를 pinata(라이브러리)를 사용해서 CID를 뽑아내서 pinata ipfs JSON파일 또한 CID를 접근해서 트랜잭션을 보낼 데이터의 객체를 생성하여 front의 axios 응답 해준다
Token List 출력
// back/src/index.ts
app.post("/api/list", async (req: Request, res: Response) => {
const deployed = new web3.eth.Contract(
SaleAbi as AbiItem[],
process.env.SALE_CA
);
let data: Array<{
tokenId?: string;
price?: string;
name: string;
description: string;
image: string;
}> = [];
if (req.body.from) {
try {
const tempArr = await deployed.methods
.getOwnerTokens(req.body.from)
.call();
for (let i = 0; i < tempArr.length; i++) {
try {
const { name, description, image } = (
await axios.get(tempArr[i].tokenURI)
).data;
data.push({
tokenId: tempArr[i].tokenId,
price: tempArr[i].price,
name,
description,
image,
});
} catch (err) {}
}
} catch (err) {}
} else {
try {
const tempArr = await deployed.methods.getSaleTokenList().call();
for (let i = 0; i < tempArr.length; i++) {
try {
const { name, description, image } = (
await axios.get(tempArr[i].tokenURI)
).data;
data.push({
tokenId: tempArr[i].tokenId,
price: tempArr[i].price,
name,
description,
image,
});
} catch (err) {}
}
} catch (err) {}
}
res.send(data);
});
- 기존에 get방식으로 axios통신을 했었지만, 계정의 주소값을 보내기에 post방식으로 변경해 준다
- abi는 Contract SaleToken의 abi값을 가져오고 CA는 env에 저장된 SALE_CA를 넣어줘서 새로운 Contract를 생성
- 메타마스크에 로그인이 됐을 때랑 안 됐을 때랑 나눠서 코드를 구현
- 메타마스크에 연결돼서 계정이 있을 때는 해당 계정을 기준으로 생성한 토큰정보들을 가져와서 목록들을 출력
- 메타마스크에 연결이 되어있지 않으면 계정에 대한 정보가 없으므로 판매 토큰 목록들을 출
// front/src/components/List.tsx
import axios from "axios";
import { useEffect, useState } from "react";
interface nftData {
name: string;
description: string;
image: string;
}
export const List = ({ account }: { account: string }) => {
const [list, setList] = useState<Array<nftData>>([]);
useEffect(() => {
(async () => {
setList(
(await axios.post("http://localhost:8080/api/list", { from: account }))
.data
);
})();
}, [account]);
return (
<ul>
{list.map((item, index) => (
<Item item={item} key={`item-${index}`} />
))}
</ul>
);
};
const Item = ({ item: { name, description, image } }: { item: nftData }) => {
return (
<li>
<div>{name}</div>
<div>{description}</div>
<div>
<img src={image} alt="" />
</div>
</li>
);
};
useWeb3.ts Custom Hook 수정
window.ethereum?.on("accountsChanged", async () => {
if (window.ethereum) {
const [_account] = (await window.ethereum.request({
method: "eth_requestAccounts",
})) as Array<string>;
setAccount(_account);
}
});
전체 코드
// front/src/App.tsx
import { useWeb3 } from "./modules/useWeb3";
import Mint from "./components/Mint";
import List from "./components/List";
function App() {
const { web3, account, chainId, logIn } = useWeb3();
return (
<div>
<div>
{account && web3 ? (
<div>
<div>ChainId : {chainId}</div>
<div>Account : {account}</div>
<Mint web3={web3} account={account} />
</div>
) : (
<div>
<button
onClick={() => {
logIn();
}}
>
LogIn MetaMask
</button>
</div>
)}
</div>
<List account={account} />
</div>
);
}
export default App;
useWeb3.ts
// front/src/modules/useWeb3.ts
import { useCallback, useState } from "react";
import Web3 from "web3";
export const useWeb3 = (): {
web3?: Web3;
account: string;
chainId: string | null | undefined;
logIn: () => void;
} => {
const [web3, setWeb3] = useState<Web3 | undefined>();
const [account, setAccount] = useState<string>("");
const [chainId, setChainId] = useState<string | null | undefined>("");
const logIn = useCallback(async () => {
try {
if (!window.ethereum) console.log("MetaMask is not exist");
const _web3 = new Web3((window as any).ethereum);
setWeb3(_web3);
const [_account] = (await window.ethereum?.request({
method: "eth_requestAccounts",
})) as Array<string>;
if (_account) setAccount(_account);
window.ethereum?.on("accountsChanged", async () => {
if (window.ethereum) {
const [_account] = (await window.ethereum.request({
method: "eth_requestAccounts",
})) as Array<string>;
setAccount(_account);
}
});
setChainId(window.ethereum?.networkVersion);
} catch (err) {
console.log(err);
}
}, []);
return { web3, account, chainId, logIn };
};
Mint.tsx
front/src/components/Mint.tsx
import axios from "axios";
import { useCallback, useState, FormEvent, ChangeEvent } from "react";
import Web3 from "web3";
const Mint = ({ web3, account }: { web3: Web3; account: string }) => {
const [NftName, setName] = useState<string>("");
const [NftDescription, setDescription] = useState<string>("");
const [file, setFile] = useState<File | undefined>();
const [img, setImg] = useState<string | ArrayBuffer>("");
const settingName = useCallback((e: FormEvent<HTMLInputElement>) => {
setName(e.currentTarget.value);
}, []);
const settingDescription = useCallback((e: FormEvent<HTMLInputElement>) => {
setDescription(e.currentTarget.value);
}, []);
const fileChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
if (e.currentTarget.files && e.currentTarget.files.length > 0) {
setFile(e.currentTarget.files[0]);
const reader = new FileReader();
reader.readAsDataURL(e.currentTarget.files[0]);
reader.onload = () => {
if (reader.result) setImg(reader.result);
};
}
}, []);
const mint = async () => {
if (!NftName || !NftDescription || !file) return;
const formData = new FormData();
formData.append("file", file);
formData.append("name", NftName);
formData.append("description", NftDescription);
formData.append("from", account);
const result = (
await axios.post("http://localhost:8080/api/mint", formData)
).data;
console.log(result);
web3.eth.sendTransaction(result);
};
return (
<div>
<input type={"text"} placeholder={"NftName"} onInput={settingName} />
<input
type={"text"}
placeholder={"NftDescription"}
onInput={settingDescription}
/>
<input type={"file"} onChange={fileChange} />
{img && (
<div>
<img src={img.toString()} alt="" />
</div>
)}
<button onClick={mint}>Start Minting</button>
</div>
);
};
export default Mint;
List.tsx
// front/src/components/List.tsx
import axios from "axios";
import { useCallback, useState, useEffect } from "react";
interface nftData {
name: string;
description: string;
image: string;
}
const List = ({ account }: { account: string }) => {
const [list, setList] = useState<Array<nftData>>([]);
useEffect(() => {
(async () => {
setList(
(await axios.post("http://localhost:8080/api/list", { from: account }))
.data
);
})();
}, [account]);
return (
<ul>
{list.map((item, index) => (
<Item item={item} key={`item-${index}`} />
))}
</ul>
);
};
export default List;
const Item = ({ item: { name, description, image } }: { item: nftData }) => {
return (
<li>
<div>{name}</div>
<div>{description}</div>
<div>
<img src={image} alt="" />
</div>
</li>
);
};
back/src/index.ts
back/src/index.ts
import express, { Express, Request, Response } from "express";
import dotenv from "dotenv";
import multer from "multer";
import cors from "cors";
import pinataSDK from "@pinata/sdk";
import { Readable } from "stream";
import Web3 from "web3";
import { AbiItem } from "web3-utils";
import axios from "axios";
import { abi as NftAbi } from "../contracts/artifacts/NftToken.json";
import { abi as SaleAbi } from "../contracts/artifacts/SaleToken.json";
const app: Express = express();
dotenv.config();
const web3 = new Web3("http://ganache.test.errorcode.help:8545");
const pinata = new pinataSDK(process.env.API_Key, process.env.API_Secret);
app.use(cors({ origin: true, credential: true }));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
const upload: multer.Multer = multer();
app.post("/api/list", async (req: Request, res: Response) => {
const deployed = new web3.eth.Contract(
SaleAbi as AbiItem[],
process.env.SALE_CA
);
let data: Array<{
tokenId?: string;
price?: string;
name: string;
description: string;
image: string;
}> = [];
if (req.body.from) {
try {
const tempArr = await deployed.methods
.getOwnerTokens(req.body.from)
.call();
for (let i = 0; i < tempArr.length; i++) {
try {
const { name, description, image } = (
await axios.get(tempArr[i].tokenURI)
).data;
data.push({
tokenId: tempArr[i].tokenId,
price: tempArr[i].price,
name,
description,
image,
});
} catch (err) {}
}
} catch (err) {}
} else {
try {
const tempArr = await deployed.methods.getSaleTokenList().call();
for (let i = 0; i < tempArr.length; i++) {
try {
const { name, description, image } = (
await axios.get(tempArr[i].tokenURI)
).data;
data.push({
tokenId: tempArr[i].tokenId,
price: tempArr[i].price,
name,
description,
image,
});
} catch (err) {}
}
} catch (err) {}
}
res.send(data);
});
app.post(
"/api/mint",
upload.single("file"),
async (req: Request, res: Response) => {
const { name, description }: { name: string; description: string } =
req.body;
const imgResult: {
IpfsHash: string;
PinSize: number;
Timestamp: string;
isDuplicate?: boolean;
} = await pinata.pinFileToIPFS(Readable.from(req.file.buffer), {
pinataMetadata: { name: Date.now().toString() },
pinataOptions: { cidVersion: 0 },
});
if (imgResult.isDuplicate) console.log("img Duplicated");
const jsonResult = await pinata.pinJSONToIPFS(
{
name,
description,
image: `https://gateway.pinata.cloud/ipfs/${imgResult.IpfsHash}`,
},
{
pinataMetadata: { name: Date.now().toString() },
pinataOptions: { cidVersion: 0 },
}
);
const deployed = new web3.eth.Contract(
NftAbi as AbiItem[],
process.env.NFT_CA
);
const obj: { nonce: number; to: string; from: string; data: string } = {
nonce: 0,
to: "",
from: "",
data: "",
};
obj.nonce = await web3.eth.getTransactionCount(req.body.from);
obj.to = process.env.NFT_CA;
obj.from = req.body.from;
obj.data = deployed.methods.safeMint(jsonResult.IpfsHash).encodeABI();
res.send(obj);
}
);
app.listen(8080, () => {
console.log("Server Opend");
});