BlockChain
(BlockChain) NFT-Market (1)
JJeongHyun
2023. 3. 16. 17:11
반응형
NFT를 거래할 수 있는 조그마한 Market을 만들어 보려고 한다
일단, 시작에 앞서 파일 및 폴더 환경설정부터 하면서 천천히 진행하려 한다
0. TS 설치
npm list -g
npm i -g typescript ts-node
- 이전에 TS를 사용한 적이 없거나 컴퓨터를 교체하여서 window를 재설치 혹은 새로 깔았다면 전역에 ts를 설치해 주자
1. React (with.TS)
yarn create react-app front --template typescript
cd front
yarn add web3 axios @metamask/providers
- @metamask/providers : TS환경에서 React내 window.ethereum의 자료형을 TS Compiler가 인지하지 못하기에 위와 같은 라이브러리를 설치
- 이후 설치된 후 front(react) 폴더 내에 있는 react-app-env.d.ts 파일내부에 아래와 같이 코드를 작성해 준다
// react-app-env.d.ts
import { MetaMaskInpageProvider } from "@metamask/providers";
declare global {
interface Window {
ethereum?: MetaMaskInpageProvider;
}
}
2. Express(back)
mkdir back
cd back
npm init -y
npm i express dotenv @openzeppelin/contracts @remix-project/remixd cors multer @pinata/sdk
npm i -D @types/node nodemon @types/express @types/multer prettier-plugin-solidity tsconfig-paths
2-1. back폴더 내 package.json 부분 코드 수정
"start": "node ./build/index.js",
"start:dev": "nodemon --watch \"src/**/*.ts\" --exec \"ts-node\" src/index.ts"
- 위처럼 JSON파일 설정 후 back폴더 위치에서 아래 명령어로 express를 실행할 예정
- npm run start:dev
3. Custom Hook 생성
// 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);
setChainId(window.ethereum?.networkVersion);
} catch (err) {
console.log(err);
}
}, []);
return { web3, account, chainId, logIn };
};
- 작성 중 react-app-env.d.ts 파일에 메타마스크 자료형 설정
- useCallback Hook을 사용함으로써 랜더링이 이루어질 때마다 함수가 선언되지 않게 설정
- 두 번째 매개변수로 빈 배열을 넣어줌으로써 최초 마운팅 될 때만 함수 선언이 되도록 설정
4. 최상위 App.tsx 컴포넌트에서 useWeb3 Hook을 가져와서 정보를 출력
// front/src/App.tsx
import { useWeb3 } from "./modules/useWeb3";
import {} from "react";
import Mint from "./components/Mint";
function App() {
const { account, chainId, logIn } = useWeb3();
return (
<div>
<div>
{account ? (
<div>
<div>ChainId : {chainId}</div>
<div>Account : {account}</div>
<Mint />
</div>
) : (
<div>
<button
onClick={() => {
logIn();
}}
>
LogIn MetaMask
</button>
</div>
)}
</div>
</div>
);
}
export default App;
- 메타마스크가 연결되어 있지 않으면 연결할 수 있는 버튼을 출력
- 해당 버튼을 누르면 현재 메타마스크의 계정으로 연결
- 연결이 되어 있으면 현재 연결된 계정의 체인정보와 계정을 브라우저에 출력
5. mint 함수를 사용하는 Mint 컴포넌트 생성
import axios from "axios";
import { useCallback, useState, FormEvent, ChangeEvent } from "react";
const Mint = () => {
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);
const result = (
await axios.post("http://localhost:8080/api/mint", formData)
).data;
console.log(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;
- 컴포넌트 내에 input onInput으로 입력에 대한 값들을 state변수에 저장
- 입력할 때마다 현재 해당 값들을 state의 set함수로 재정의
- 이렇게 state값을 설정하는 함수는 랜더링 될 때마다 불필요하게 선언될 필요가 없기에 useCallback Hook으로 막아준다
- 업로드할 이미지를 미리 보기를 위해서 업로드 input에 대한 change함수 생성
- onChange 이벤트를 통해서 값이 변화가 있을 때마다 현재 해당 값이 있으면서 해당 값의 길이가 0보다 클 때 set함수를 통하여 이미지의 값을 재정의한다
- 이후 내장되어 있는 파일을 읽는 객체를 만들어서 내용을 가지고 element, 즉 브라우저에 출력해 주기 위해 준비
- 로딩이 되면 그 결과값을 setImg함수로 state img 변수를 재정의 한다
- 그 결과값을 img태그의 src에 toString() 메서드로 이미지주소 값으로 변환하여 접근한다
- NFT이름과 설명 그리고 이미지까지 state변수에 저장이 되었다면 axios요청 시 전달 데이터로 넣어준다
- 파일 업로드를 위해서 express 환경에서 multer라이브러리를 사용하기 때문에 formData라는 내장 객체에 append 하여 axios.post 요청할 때 같이 보내준다
6. axios에 요청에 대한 함수를 만들었기에 express 환경 설정
tsconfig.json생성
{
"exclude": ["node_modules"],
"compilerOptions": {
"outDir": "./build/",
"target": "ES6",
"lib": ["ES6", "DOM"],
"moduleResolution": "node",
"esModuleInterop": true,
"resolveJsonModule": true,
"removeComments": true,
"allowJs": true,
"baseUrl": ".",
"typeRoots": ["./node_modules/@types", "./@types"],
"paths": {
"@core/*": ["src/core/*"],
"*": ["@types/*"]
}
},
"ts-node": {
"files": true,
"require": ["tsconfig-paths/register"]
}
}
src/index.ts
import { Readable } from "stream";
// stream은 nodejs가 가지고 있는 내장라이브러리다
// 데이터를 stream화 해주는 라이브러리
const pinata = new pinataSDK(process.env.API_Key, process.env.API_Secret);
// pinata 페이지 로그인 후 API Keys를 눌러서 새로운 키를 발급받고 그 후에 나오는 Key, secret를 env파일에 넣어준다
back/.env
API_Key= pinata웹페이지에서 API_KEY값
API_Secret= pinata웹페이지에서 API_SECRET값
7. axios 응답 라우터 코드 작성
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 },
}
);
res.send("mint Complete");
}
);
- upload.single : multer를 사용하여 formData에 전달된 input type file이 한 개이기에 single로 axios 응답 쪽에 설정
- const {name, description} : {name:string, description:string} = req.body;
- 구조분해할당을 통해 NFT이름과 설명의 변수명과 값을 그대로 가져와서 사용한다
- imgResult : NFT이미지로 사용할 이미지를 pinata를 이용해서 올린다
- jsonResult NFT JSON파일을 pinata를 이용해서 올린다
8. List 컴포넌트를 생성
import axios from "axios";
import { useCallback, useState, useEffect } from "react";
interface nftData {
name: string;
description: string;
image: string;
}
const List = () => {
const [list, setList] = useState<Array<nftData>>([]);
const listUp = useCallback(async () => {
const result = (await axios.get("http://localhost:8080/api/list")).data;
setList(result.tempArr);
}, []);
useEffect(() => {
listUp();
}, []);
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>
);
};
- pinata. IPFS에 업로드한 전체 JSON파일의 목록들을 출력
- interface를 따로 선언해서 코드의 중복과 가독성을 높여준다
- interface nftData 선언 없이 한다면 다음과 같다
const [list, setList] = useState<
Array<{
name: string;
description: string;
image: string;
}>
>([]);
const Item = ({
item: { name, description, image },
}: {
item: {
name: string;
description: string;
image: string;
};
}) => {
return (
<li>
<div>{name}</div>
<div>{description}</div>
<div>
<img src={image} alt="" />
</div>
</li>
);
};
9. List 컴포넌트를 완성 후 임시 배열들을 express 환경에 선언해 주고 axios.get 요청에 대한 응답으로 가져와서 list.map 메서드를 실행한다
app.get("/api/list", (req: Request, res: Response) => {
const tempArr = [
{
name: "test NFT",
description: "testing NFT with Pinata",
image:"https://gateway.pinata.cloud/ipfs/Qmcc65wxFtcJyEtEU1riHMUP8a7Vr4gJTxezoyQZCWyazu",
},
{
name: "test NFT",
description: "testing NFT with Pinata",
image:"https://gateway.pinata.cloud/ipfs/Qmcc65wxFtcJyEtEU1riHMUP8a7Vr4gJTxezoyQZCWyazu",
},
{
name: "test NFT",
description: "testing NFT with Pinata",
image:"https://gateway.pinata.cloud/ipfs/Qmcc65wxFtcJyEtEU1riHMUP8a7Vr4gJTxezoyQZCWyazu",
},
];
res.send({ tempArr });
});