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 });
});