본문 바로가기
포트폴리오/Project_3

P3: KiFT Community-Curated NFT MarketPlace

by StelthPark 2022. 2. 4.

1. 역할과 기여

  1. 팀원
  2. 프론트엔드 구현
  3. React 구조 설계
  4. 레이아웃 및 인터페이스 구성
  5. UI 사용자 조건 제어 설계
  6. NFT Item 히스토리 UI 구현
  7. File System, IPFS 설계 및 구현
  8. NFT 민팅 페이지 구현
  9. KiFT 마켓 컨트랙트 설계 (Solidity)
  10. 데이터베이스

2. 프로젝트 소개

 

1. KiFT 란

KiFT는 디지털 NFT 마켓플레이스와 더불어 예술 큐레이션 및 인센티브 기반 토큰 배포 기능이 추가된 디지털 통합 환경이다. 또한 사용자들은 NFT를 직접 만들어서 배포할 수 있는 기능도 포함되어 있다. 자신이 원하는 NFT를 사고 팔고, 그 활동의 대가로 커뮤니티 고유 토큰인 KFT 토큰을 받을 수 있는 환경이다.

 

2. KiFT 만의 장점

 1. 뛰어난 가시성으로써 유저들의 편리한 사용을 위해 간단하지만 완벽하게 디자인 된 웹사이트

 2. 메타마스크(링크비)와 카이카스(바오밥)을 모두 지원하며 유저가 원하는 방식으로 로그인가능

 3. 높은 가독성으로 쉬워진 접근성을 제공하며 NFT의 원하는 판매가격만 적으면 판매할 수 있도록 설계 및 구현완료

 4. KiFT만의 간편하고 빠른 NFT 민팅을 통해 누구 손쉽게 NFT 제작을 할 수 있으며 접속 네트워크에 따라 KIP17과 ERC721을 스위칭하여 민팅가능

 5. Search를 이용한 나만의 NFT 아카이브를 생성 할 수 있고 검색을 통해 프라이빗한 NFT아카이브를 생성 할 수 있어 유저들간의 다이렉트 구매과정을 가능하게 함

 6. 무한한 NFT가능성에 뒷 받침 될 NFT의 신뢰성을 위해 제공되는 KiFT의 엄선된 큐레이티드와 자체 클레임 토큰을 통해 거래기여도 향상과 유동성 증가 이후 탈중화 거래소(DEX)를 통한 거래 및 DAO생태계 구축

 7. 자체 클레임 토큰을 사용해 스테이킹 할 수 있으며 이자수익을 창출 할 수 있음

 


3. 세부 기능 

  1. 카이카스 및 메타마스크로 로그인 및 모든 서명
  2. 마이페이지에서 현재 address가 보유한 NFT 아이템 모두 출력
  3. 보유중인 NFT 아이템 중 다른 address로 전송(트랜스퍼) 기능
  4. 보유중인 NFT 아이템 중 마켓에 판매 등록(리스트) 기능
  5. 마켓에 판매 등록되어 있는 모든 NFT아이템 출력 및 구매 기능
  6. NFT 구매가 이루어지면 KiFT 마켓 컨트랙트 내부에 설정 된 listingPrice(초기 0.00025ETH) 서버에게 전송 및 판매자에게 listingPrice제외하고 판매대금 전송
  7. 마켓에 등록된 NFT 아이템의 가격변경 및 취소(언리스트) 기능
  8. 모든 NFT아이템의 민팅, 리스트, 가격변경, 취소(언리스트), 전송(트랜스퍼), 거래 기록을 히스토리에 기록 및 출력
  9. 큐레이트페이지에서 KiFT가 선정한 NFT 표시
  10. KiFT내 NFT 아이템 판매 등록(리스트) 및 거래가 성사될 시 기여도 부여
  11. 클레임토큰페이지에서 각 address별 기여도에 따른 KFT토큰 민팅
  12. KFT토큰을 스테이킹하여 이자로 KFT 토큰을 부여
  13. KiFT 자체 KIP-17, ERC-721 컨트랙트를 통해 컨텐츠, 이름, 콜렉션, 설명, 프로퍼티 등을 기입해 NFT 민팅 가능
  14. KiFT에서 한번이라도 로그인 한 기록이 있는 address의 모든 NFT는 Search를 통해 검색 가능

4. 조건

  1. 카이카스와 메타마스크가 동시에 로그인되어 있을 시 카이카스를 중심으로 Baobab환경이 작동됩니다.
  2. 아무것도 로그인되어 있지 않을시 마켓에서는 Rinkeby NFT를 출력합니다.
  3. 구글 크롬 확장 프로그램으로 두 지갑이 설치되어있지 않을 시 설치 알람과 함께 설치페이지가 작동됩니다.
  4. CreateNFT 페이지에서는 현재 로그인한 지갑 중 중심네트워크에 따라 자동으로 해당 네트워크를 스위칭하여 민팅합니다.
  5. Claim, Staking 페이지는 Baobab환경과 KIP-7의 KFT토큰을 사용합니다.
  6. Nav.js 컴포넌트에서 Rinkeby환경과 ERC-20 KFT토큰도 Claim, Staking 페이지 사용을 위해 주석처리되어있고 관련 컴포넌트와 로직이 작성되어있습니다.
  7. Staking은 별도의 예치시간이나 이자율 등 조건이 계산되진 않았습니다.
  8. DAO는 구현 중에 있으며 클릭시 메인화면으로 이동합니다.

5. 개발 방향 및 사용 기술

우선 우리팀은 시중에 많은 NFT거래소들이 있다는 것을 파악하였고 그 중에서도 유저들이 가장 많이 이용하는 오픈씨 거래소가 이더리움 가스 스테이션에서 가장 많은 가스비를 소모하고 있다는 것을 확인하였다. 이러한 현상에서 우리는 어떻게 하면 더 나은 NFT거래 환경을 유저들에게 제공할 수 있을까 고민하였고 마켓과 유저들이 서로 상생할 수 있고 NFT를 사고팔면서 쌓은 기여도를 통해 우리가 발행한 ERC20 토큰도 받고 그 토큰을 통해 스테이킹과 탈중앙화 조직인 DAO형태로 홀더들이 직접 마켓의 발전방향성과 안건에 대해 투표할 수 있게 하여 마켓과 유저들이 서로 윈윈하는 방향을 그리게 되었다. 팀원이 나를 포함한 2명이기 때문에 효율적으로 시간을 관리하고 개발에 임하기 위해서는 우선적으로 구현할 기능들을 정리하였고 거래시스템-민팅-히스토리-클레임-스테이킹-DAO 순으로 계획하였다.

 

또한 한국인에게 더 접근하기 쉬운 NFT 거래 환경을 제공하고 한국인에게 친화적인 클레이튼을 중점으로 서비스 할 수 있도록 최종 목표를 정하였고 또한 현재 클레이튼 체인에서는 이렇다할 NFT 마켓이 존재하지 않아 꼭 필요할 것 같다는 판단과 동시에 클레이튼 체인과 이더리움 체인을 둘다 사용하면 조금 더 범용성이 좋아지지 않을까 하는 생각에 메타마스크와 카이카스를 동시에 사용하게 하였다.

 

개발과정에서 사용한 네트워크는 오픈씨의 거래과정을 분석하고 불필요한 행위를 없애며 가볍고 쉬운 NFT 아이템 마켓등록을 구현하고자 오픈씨 테스트넷에서 사용하는 이더리움 링크비 네트워크를 타겟으로 개발을 시작하였고 대부분 거래소에서 사용하는 ERC721의 SetApproveForAll을 통해 권한을 받고 KiFT자체 마켓거래 컨트랙트에 해당 NFT를 리스팅하는 방식으로 설계하였다. 카이카스 지갑 환경에서는 당연 바오밥 테스트 네트워크 환경에서 모든 행위가 이루어지게 될 것이다.

 

ERC-721로 보유한 NFT을 불러오는 건 비교적으로 쉬웠으나 KIP-17로 만든 NFT는 노드를 돌리지않고 어떻게 보유한 NFT의 컨트랙트 주소를 받아오고 그 주소를 API로 날려 NFT 메타데이터를 받을 지 고민을 많이했다. 카이카스로 거래하는 실 서비스 NFT마켓플레이스를 보면 자신들이 만든 KIP 민팅 컨트랙트에서 민팅한 NFT만 마이페이지에서 불러오고 거래할 수 있는 형식으로 구성되는 곳이 많았다. 그래야만 NFT 컨트랙트 주소가 정해져있고 메타데이터를 불러오기 쉬우니까. 우리는 이를 클레이튼 스코프를 이용하기로 했다. 지갑주소를 입력하면 KIP17의 발란스를 가져오고 해당하는 컨트랙트 주소를 웹에서 HTML소스로 보여주고 있었고 이를 크롤링하여 우리 서버로 API를 날리고 메타데이터를 받는 형식으로 만들고자 했다.

 

클라이언트에서는 리액트 프레임워크와 서버간의 통신을 위해 AXIOS를 주로 사용했으며 CSS부분에서는 더 간결하고 재사용성과 가독성을 높이기 위해 Sass를 사용했다. 백엔드는 MongoDB로 유저계정과 NFT에 대한 스키마를 작성하여 클라이언트단과 통신 할 수 있도록 하였고 블록체인영역에서는 각 개인의 NFT발행을 위해 분산파일시스템인 IPFS, 마켓에서는 로그인과 서명을 위해 메타마스크와 카이카스를, 솔리디티를 활용한 KiFT 마켓 컨트랙트와 KiFT 스테이킹 컨트랙트작성 그리고 Web3.js와 Caver.js를 통해 소통을 하였다. 블록체인을 가장 활용하기 위해 백엔드와 통신하는 부분을 최소화하고 프론트단에서 유저의 행위에 의해서 블록체인에 바로 접근하여 처리결과를 얻을 수 있도록 탈중앙화에 노력하였다.

 

 

[Stack]

 

6. 데이터베이스 구조

 

7. 데이터 플로우


8. KiFT 마켓 컨트랙트 (Solidity)와 거래 과정

< NFT 판매 리스팅 >

    function createMarketItem(
        address nftContract,
        uint256 tokenId,
        uint256 price) public payable nonReentrant{
        require(price > 0, "Price must be above zero");
        require(IERC721(nftContract).ownerOf(tokenId) == msg.sender, "You aren't NFT Owner!");
        // NFT 소유주가 맞는지 검사한다
         _itemIds.increment(); //add 1 to the total number of items ever created
         uint256 itemId = _itemIds.current();

         idMarketItem[itemId] = MarketItem(
             itemId,
             nftContract,
             tokenId,
             payable(msg.sender), //address of the seller putting the nft up for sale
             payable(address(0)), //no owner yet (set owner to empty address)
             price,
             false
         );
            //log this transaction
            emit MarketItemCreated(
                itemId,
             nftContract,
             tokenId,
             msg.sender,
             address(0),
             price,
             false);

        }

해당 함수가 실행 되기전 판매자는 자신의 NFT를 KiFT 마켓컨트랙트 Address에 setApprovalForAll을 통해 권한을 위임하고 아래에서 NFT구매 함수가 발동될 때 위임받은 KiFT 마켓 컨트랙트 Address가 createMarketSale 함수 내부에서 IERC721(nftContract).transferFrom을 작동시키는 msg.sender가 되게 된다.

 

< setApprovalForAll >

판매자가 NFT를 Sell 하기전에 setApprovalForAll의 과정으로 이미 한번 approve가 된 NFT 컨트랙트주소는 가스비를 날리지 않도록 해당 과정을 작동하지않고 바로 판매 리스팅 함수를 발동시킨다.

 

< NFT 구매 >

function createMarketSale(
            address nftContract,
            uint256 itemId
            ) public payable nonReentrant{
                uint price = idMarketItem[itemId].price;
                uint tokenId = idMarketItem[itemId].tokenId;

                require(msg.value == price, "Please submit the asking price in order to complete purchase");
                // 구매자가 전송한 value가 현재 리스팅 된 NFT 가격과 일치하는지 검사
                require(idMarketItem[itemId].sold == false, "This nft cannot be purchased. isSOLD.");
          		// 현재 리스팅되어 있는 NFT가 맞는지 검사
          //pay the seller the amount
           idMarketItem[itemId].seller.transfer(msg.value-listingPrice);
           // 판매자에게 리스팅비용을 제외한 value 전송
            payable(owner).transfer(listingPrice);
            // 컨트랙트를 만든 주인에게 리스팅비용 전송
             //transfer ownership of the nft from the contract itself to the buyer
            IERC721(nftContract).transferFrom(idMarketItem[itemId].seller, msg.sender, tokenId);
  			// 구매자에게 NFT 전송
  
            idMarketItem[itemId].owner = payable(msg.sender); //mark buyer as new owner
            idMarketItem[itemId].sold = true; //mark that it has been sold
            _itemsSold.increment(); //increment the total number of Items sold by 1
             //pay owner of contract the listing price
        }

구매자가 Buy를 누를시 작동되는 NFT구매함수로 2개의 require를 통과한 뒤 KiFT 마켓컨트랙트가 초기값으로 지정 해 둔 리스팅비용인 0.00025 ether만큼 판매가격에서 제외하고 판매자에게 전송, KiFT 마켓컨트랙트 주인에게 리스팅비용을 전송, 구매자에게 NFT전송의 과정을 거치게 된다. 리스팅 비용은 KiFT 마켓컨트랙트 내부에 있는 getListingPrice와 setListingPrice를 통해 현재 리스팅비용을 확인하거나 변경할 수 있다. 현재는 간단하게 리스팅비용을 고정식으로 했지만 내부 로직을 변경해서 NFT가격에 따른 비율을 계산해서 리스팅비용을 계산하도록 할 수도 있다.

 

< 현재 ListingPrice 확인/변경 >

    function getListingPrice() public view returns (uint256){
        return listingPrice;
    }

      /// @notice function to get listingprice
    function getOwner() public view returns (address){
        return owner;
    }

 

<현재 판매중인 NFT 가격변경/언리스트 >

 

            function changeMarketItemPrice(
            uint256 itemId,
            uint256 price
            ) public payable nonReentrant{
                require(msg.sender == idMarketItem[itemId].seller, "Please submit this NFT seller");
                // 아이템 판매자가 맞는지 확인
                require(idMarketItem[itemId].sold == false, "This nft is not on sale. isNotSOLD");
           		// 현재 아이템이 판매되지 않았는지
           idMarketItem[itemId].price = price;
			//가격 변경
        }

         function soldOutMarketItem(
            uint256 itemId
            ) public payable nonReentrant{
                require(msg.sender == idMarketItem[itemId].seller, "Please submit this NFT seller");
                // 아이템 판매자가 맞는지 확인	
                require(idMarketItem[itemId].sold == false, "This nft is not on sale. isNotSOLD");
           idMarketItem[itemId].sold = true;
           // 판매 처리
        }

판매중인 아이템을 언리스트하기위해 soldoutMarketItem 함수를 작동시키면 해당 판매중인 아이템 sold가 마치 아이템이 Buy될 때 처럼 true로 바뀌게되고 우리가 보관중인, 우리의 웹에 로그인했던 모든 유저가 가진 NFT들중에 해당하는 그 아이템의 isSale 컬럼이 false로 바뀌게된다. 이후에 다시 Sell을 하게 되면 새롭게 블록체인에 올리게 되며 새 itemId를 부여하게 되며 KiFT에서 관리중인 NFT DB에서는 itemIdOnBlockChain넘버가 새 itemId로 바뀌게되며 다시 등록된다.


9. KiFT NFT 민팅

const web = new Web3(metamaskProvider);
        web.eth.getAccounts().then(async (account) => {
          if (account.length === 0) {
            setClosebox(true);
            setMessage("Please log in to MetaMask");
          } else {
            const imgURI = await ipfs.add(files);
            const metadata = {
              name: name,
              collection: collection,
              description: description,
              image: `https://ipfs.io/ipfs/${imgURI.path}`,
              attributes: resultTraits,
            };
            const tokenUri = await ipfs.add(JSON.stringify(metadata));
            const newTokenURI = `https://ipfs.io/ipfs/${tokenUri.path}`;
            console.log("test", newTokenURI);
            let contract = await new web.eth.Contract(KiFT721abi, Kift_721_Contract_Address);

            await contract.methods
              .mintNFT(newTokenURI)
              .send({
                from: account[0],
                gas: 500000,
                gasPrice: "2450000000",
              })

우리의 웹에서 Create NFT 페이지에서 이미지 사진을 업로드하면 해당 FileReader를 사용해 state에 담고 그것을 ipfs에 add하여 imgURI를 가져오게된다. 메타데이터를 만들기 위해 다시 해당 imgURI를 값으로 넣어 생성하고 다시 ipfs에 add하여 tokenUri로 받아 https://ipfs.io/ipfs/${tokenUri.path}를 사용해 최종적인 tokenURI를 만들게 된다.

미리 발행한 KiFT의 ERC-721은 내부에서 ownable을 제거했으므로 누구나 NFT를 tokenURI만 있으면 자신에게 민팅 할 수 있다.

 

< KiFT ERC-721 >

//Contract based on [https://docs.openzeppelin.com/contracts/3.x/erc721](https://docs.openzeppelin.com/contracts/3.x/erc721)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;

import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/utils/Counters.sol";
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";

contract KNiFT is ERC721URIStorage {
    using Counters for Counters.Counter;
    Counters.Counter private _tokenIds;

    constructor() public ERC721("KNiFT", "KNFT") {}

    function mintNFT(string memory tokenURI)
        public 
        returns (uint256)
    {
        _tokenIds.increment();

        uint256 newItemId = _tokenIds.current();
        _mint(msg.sender, newItemId);
        _setTokenURI(newItemId, tokenURI);

        return newItemId;
    }
}

10. KiFT Claim Token

우리의 웹을 사용하여 NFT를 리스팅하거나 구매하는 거래과정에 참여한 판매자와 구매자에는 기여도를 부여받고 기여도를 일정 비율에 따라 KFT라는 토큰을 민팅 받을 수 있다. 거래과정의 끝에 DB를 처리하는 부분에서 해당 하는 User마다 point컬럼의 숫자를 올리게 되고 해당 숫자만큼 Claim KiFToken 페이지에서 민팅 컨트랙트를 통해 본인에게 민팅받게 된다.

 

< 민팅 버튼 로직 >

 try {
        const web = new Web3(metamaskProvider);
        web.eth.getAccounts().then(async (account) => {
          //계정 조회후 포인트 받아옴
          await axios
            .post("http://ec2-3-36-70-55.ap-northeast-2.compute.amazonaws.com:3001/findUser", {
              address: account[0].toLowerCase(),
            })
            .then((result) => {
              console.log("how many got token======>>>>", result.data.data.points);
              return result.data.data.points;
            })
            .then(async (point) => {
              //포인트 있으면 민트 가능!!
              if (point > 0) {
                let numbersUserCanClaim = point * 7 * 1000000000000000000;
                console.log(numbersUserCanClaim.toString());
                let contract = await new web.eth.Contract(KiFTTokenabi, process.env.REACT_APP_KIFT_TOKEN_CONTRACT_ADDRESS);
                await contract.methods
                  .mintToken(account[0], numbersUserCanClaim.toString())
                  .send({
                    from: account[0],
                    gas: 100000,
                    gasPrice: "10000000000",
                  })
                  .then(async (receipt) => {
                    console.log(receipt);
                    if (receipt.blockHash) {
                      //민트 성공하면 디비 초기화 !!
                      await axios
                        .post("http://ec2-3-36-70-55.ap-northeast-2.compute.amazonaws.com:3001/initializePoints", {
                          address: account[0].toLowerCase(),
                        })

 

< KFT 토큰 컨트랙트 >

// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.10;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";


contract KiFTToken is ERC20, Ownable {
    constructor() ERC20("KiFT_Token", "KFT") {
    }

    function mintToken(address to, uint256 amount) public returns (bool){
        require(to != address(0x0));
        require(amount > 0);
        _mint(to, amount);

        return true;
    }
}

11. KiFT Token 스테이킹

클레임으로 받은 KFT 토큰을 스테이킹 하기 위해 우선 ERC20으로 클레임을 받을수 있지만 프로젝트 목적상 KIP7으로 받아 카이카스를 이용한 스테이킹을 구현했다. 스테이킹 할 양을 KiFT 스테이킹 컨트랙트에 Approve하고 스테이킹 함수를 실행시켜 예치하게 된다. 만약 이후에 추가적인 스테이킹을 하게 된다면 Approve된 양이 충분하다면 가스비를 낭비하는 추가적인 Approve는 하지않고 바로 예치를 하게된다. 스테이킹 컨트랙트에는 별도로 예치조건이나 이자율이 계산 되진 않았다. 실서비스를 한다면 이를 조금 더 수정할 예정이다.

 

< KFT 스테이킹 컨트랙트 >

// SPDX-License-Identifier: MIT
pragma solidity 0.5.16;

contract StakingRewards {
    IERC20 public rewardsToken;
    IERC20 public stakingToken;

    uint public rewardRate = 1000;
    uint public lastUpdateTime;
    uint public rewardPerTokenStored;

    mapping(address => uint) public userRewardPerTokenPaid;
    mapping(address => uint) public rewards;

    uint private _totalSupply;
    mapping(address => uint) private _balances;

    constructor(address _stakingToken, address _rewardsToken) public {
        stakingToken = IERC20(_stakingToken);
        rewardsToken = IERC20(_rewardsToken);
    }

    function rewardPerToken() public view returns (uint) {
        if (_totalSupply == 0) {
            return 0;
        }
        return
            rewardPerTokenStored +
            (((block.timestamp - lastUpdateTime) * rewardRate * 1e18) / _totalSupply);
    }
    
    function earned(address account) public view returns (uint) {
        // 현재 나의 이자 총액
        return
            ((_balances[account] *
                (rewardPerToken() - userRewardPerTokenPaid[account])) / 1e18) +
            rewards[account];
    }

    function stakingValue(address account) public view returns (uint) {
        return _balances[account];
    }
    
    
    function totalSupply() public view returns (uint) {
        // 컨트랙트에 총 스테이킹 중인 양
        return _totalSupply;
    }

    modifier updateReward(address account) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = block.timestamp;

        rewards[account] = earned(account);
        userRewardPerTokenPaid[account] = rewardPerTokenStored;
        _;
    }

    function stake(uint _amount) external updateReward(msg.sender) {
        // 스테이킹 시작
        _totalSupply += _amount;
        _balances[msg.sender] += _amount;
        stakingToken.transferFrom(msg.sender, address(this), _amount);
    }

    function withdraw(uint _amount) external updateReward(msg.sender) {
        // 예치 중인 스테이킹 출금
        _totalSupply -= _amount;
        _balances[msg.sender] -= _amount;
        stakingToken.transfer(msg.sender, _amount);
    }

    function getReward() external updateReward(msg.sender) {
        // 현재 이자 출금
        uint reward = rewards[msg.sender];
        rewards[msg.sender] = 0;
        rewardsToken.transfer(msg.sender, reward);
    }
}

interface IERC20 {
    function totalSupply() external view returns (uint);

    function balanceOf(address account) external view returns (uint);

    function transfer(address recipient, uint amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint);

    function approve(address spender, uint amount) external returns (bool);

    function transferFrom(
        address sender,
        address recipient,
        uint amount
    ) external returns (bool);

    event Transfer(address indexed from, address indexed to, uint value);
    event Approval(address indexed owner, address indexed spender, uint value);
}

12. KIP-17 NFT 모두 불러오기

특정 지갑이 소유한 KIP-17 토큰의 메타데이터를 불러와야 거래할 수가 있다. ERC-721를 지원하는 OpenSea API는 지갑주소만 페이로드로 요청하면 소유NFT부터 메타데이터까지 모두 불러왔으나 KIP-17은 NFT의 컨트랙트 주소가 필요 했고 내가 소유한 NFT들의 컨트랙트를 어디서 다 불러 올지 고민하다가 클레이튼 스코프를 확인하게 되었다. 클레이튼 스코프에서 특정 지갑주소를 입력하면 KIP 관련 발란스들을 모두 한눈에 볼 수 있었고 이를 HTML소스로 열어보면 a태그에 컨트랙트 주소들이 모두 나타나는 것을 볼 수 있었다. 

 

 

이를 크롤링 하여 컨트랙트 주소들을 이용하면 메타데이터를 얻을 수 있을 것이다 생각했고 클라이언트에서 서버로 자신의 지갑주소를 페이로드로 날리면 서버에서 받고 puppeteer와 cheerio를 이용한 크롤링을 하기로 했다. KIP17 Balance 버튼을 눌러야 나오는 html소스이므로 정적으로 크롤링하면 안되기 때문에 두 개의 모듈을 사용하였다.

 

< 서버단 크롤링 >

router.post("/crolling", async function (req, res) {
  //KIP-17의 보유 nft를 컨트랙트 주소를 모두 가져오기 위해 크롤링을 한다.
  // 브라우저를 실행한다.
  // 옵션으로 headless모드를 끌 수 있다.
  let account = req.body.account;
  const browser = await puppeteer.launch({
    headless: true, //가상웹을 띄우지 않고 back에서 처리
  });

  // 새로운 페이지를 연다.
  const page = await browser.newPage();
  // 페이지의 크기를 설정한다.

  await page.goto(`https://baobab.scope.klaytn.com/account/${account}?tabId=kip17Balance`); //해당 웹으로 이동
  //

  try {
    await page.waitForSelector(
      "#root > div > div.SidebarTemplate > div.SidebarTemplate__main > div > div > div.DetailPageTableTemplate > div > div.Tab__content > section > article.TokenBalancesList__body > div > div.Table__tbody"
    ); //해당하는 셀럭터가 다만들어질때 까지 대기

    const content = await page.content(); //html 소스 추출
    let list = [];
    const $ = cheerio.load(content); //추출된 소스를 다시 html형식으로
    const $bodyList = $(
      "div.Table__td.TokenBalancesListDesktop.TokenBalancesListDesktop__kip17-balance__name.TokenBalancesListDesktop.TokenBalancesListDesktop__kip17-balance__nameTd > a"
    ); //셀럭터 검색으로 queryselectorAll과 같은기능
    for (let i = 0; i < $bodyList.length; i++) {
      list.push($bodyList[i].attribs.href.slice(5));
    }
    await browser.close();
    res.status(200).send(list);
  } catch (e) {
    await browser.close();
    res.status(200).send("false");
  }
});

13. 개발 회고

Keep: KiFT 마켓 컨트랙트 내부에서 작동할 수 있는 것은 최대한 서버단에서 비동기 처리를 하지않고 처리하였다. 탈중앙화에 최대한 가깝게 하기 위해 서버단에서 하는 역할은 클레임토큰에 활용될 기여도와 마켓에 올린 상품을 걸러주는 역할만으로 한정하였고 대부분 클라이언트단에서 사용자가 프론트에 보이는 버튼을 통해 블록체인으로 바로 통신하여 원하는 행위를 이루어 질 수 있도록 하였다. 또한 NFT를 불러오는 과정 같이 어떤 NFT 거래가 이루어지거나 전송되는 과정이 있는 직후 바로 바로 프론트에 적용되지 않는 부분이 있어 판매해버린 NFT를 다시 올리려고 시도하거나 소유 NFT가 이제 아닌 것에 어떤 시도를 하는것을 막기 위해 1차적으로 KiFT 마켓 컨트랙트 내부에서 대부분 require문으로 소유주 검사를 하여 revert 시켰다. 또 2차적으로는 About 페이지에서 NFT가 판매되거나 구매되어 소유주가 바뀌게 되면 바로 페이지를 새로 렌더링하면서 동시에 우리의 NFT DB에 담긴 소유주를 변경시켜 프론트에 뿌려줌과 동시에 소유주가 아니면 행위할 수 없도록 If문 처리를 하였다. 실제로 웹을 개발하고 직접 판매/구매를 작동시키면서 발생하는 이슈를 바로바로 처리하면서 제어하였기에 잘못된 행위를 하는 사용자를 많이 제어 하였다고 생각한다. 또한 NFT를 민팅 할 때 로그인 한 지갑에 따라 네트워크가 스위칭되어 ERC 혹은 KIP로 민팅이 된다. KIP17의 소유 NFT를 우리의 KiFT KIP-17 민팅 컨트랙트를 통한 민팅이 아니더라도 모두 불러올 수 있게 하고 싶었고 노드없이 해결하기 위해 클레이튼 스코프를 크롤링하는 과정을 사용하였다.

 

Problem: Keep에 작성한 것처럼 웹을 실제 작동시키면서 생기는 이슈를 바로 처리하고 새 로직을 작성하다보니 특정 컴포넌트가 잘 정리되지 않고 작성자만이 이해할 수 있을 것 같이 난잡해졌다. 그리고 현재 판매를 리스팅하는 함수 내부에서 setApprovalForAll이 없는데 이는 About 페이지에서 Buy를 누르면 먼저 비동기 처리에 따라 판매자가 직접 Approve하고 그 다음순서로 넘어가는 과정을 겪고 있다. 이는 오픈씨에서 리스팅하는 방법과 유사하나 델리게이트콜로 해결할 수 있나? 라는 생각을 해보면서 시도해보고 있다. 또한 지금 서버단에서 관리하기 위한 마켓에 등록된 NFT를 불러오는 DB나 기여도 포인트 부분을 블록체인에서 관리해야한다는 점이 있다. 그리고 KIP-17 소유 NFT를 모두 불러오는 과정에서 크롤링이 사용되니 처리시간이 느리고 메타마스크로 접속한 마이페이지보다 카이카스로 접속한 마이페이지에서 속도가 많이 저하됐다. 이를 크롤링을 최적화 하거나 다른 방법으로 구현해봐야겠다.

 

Try: 델리게이트콜로 setApprovalForAll을 리스팅하는 함수내부에서 해결해보는 것과 DB에서 관리중인 몇가지 항목들을 블록체인에서 관리 할 수 있도록 시도해보며 UI NAV에 있는 KFT 중심 DAO 생태계도 구현해보자. KIP-17 소유 NFT를 불러오는 과정을 크롤링없이 다른 방법을 사용해봐야겠다.

 

14. 작동 화면

< Main Page >

 

 

 


 

< Login MetaMask or Kaikas>

 

 

 

 


< Create NFT >

 

 

 


< Mypage >

 

 

 


< NFT Transfer >

 

 

 


< NFT Listing >

 

 

 


< Market >

 

 

 


< NFT Unlisting/ChangePrice >

 

 

 


< NFT Buying >

 

 

 


< Curated >

 

 

 


< Claim Token >

 

 

 


< Staking Token >

 

 

 


15. 코드 

 


< Github >

 

GitHub - Parkstelth/Project03_KiFT-Community-Curated-NFT-MarketPlace

Contribute to Parkstelth/Project03_KiFT-Community-Curated-NFT-MarketPlace development by creating an account on GitHub.

github.com

 

< Team Notion >

 

5팀 - KiFT

1. 팀 소개

codestates.notion.site

 

 


<Test Rinkeby KiFT Market Contract>

 

Contract Address 0xC51949A7BC79bD9aADF8D272dcb048E35baa6604 | Etherscan

The Contract Address 0xC51949A7BC79bD9aADF8D272dcb048E35baa6604 page allows users to view the source code, transactions, balances, and analytics for the contract address. Users can also interact and make transactions to the contract directly on Etherscan.

rinkeby.etherscan.io

 

<Test Baobab KiFT Market Contract>

 

Klaytnscope

Klaytnscope allows you to find data by monitoring network health and statistics of Klaytn as well as profiling blocks and transactions on Klaytn.

baobab.scope.klaytn.com

 

<Test Rinkeby KiFT Staking Contract>

 

Contract Address 0x709B1dD7A9355d8DEcF1784450128c18ED32f822 | Etherscan

The Contract Address 0x709B1dD7A9355d8DEcF1784450128c18ED32f822 page allows users to view the source code, transactions, balances, and analytics for the contract address. Users can also interact and make transactions to the contract directly on Etherscan.

rinkeby.etherscan.io

 

<Test Baobab KiFT Staking Contract>

 

Klaytnscope

Klaytnscope allows you to find data by monitoring network health and statistics of Klaytn as well as profiling blocks and transactions on Klaytn.

baobab.scope.klaytn.com

 

<Test KiFT ERC-721 minting>

 

Contract Address 0x06d37ea487607Cb9De3E1972eb13DB843823F069 | Etherscan

The Contract Address 0x06d37ea487607Cb9De3E1972eb13DB843823F069 page allows users to view the source code, transactions, balances, and analytics for the contract address. Users can also interact and make transactions to the contract directly on Etherscan.

rinkeby.etherscan.io

 

<Test KiFT KIP-17 minting>

 

Klaytnscope

Klaytnscope allows you to find data by monitoring network health and statistics of Klaytn as well as profiling blocks and transactions on Klaytn.

baobab.scope.klaytn.com

 

<Test KFT ERC-20>

 

Contract Address 0x34547466Db1b5A72AE6AeEd1Ac8C2CA6DB3E381e | Etherscan

The Contract Address 0x34547466Db1b5A72AE6AeEd1Ac8C2CA6DB3E381e page allows users to view the source code, transactions, balances, and analytics for the contract address. Users can also interact and make transactions to the contract directly on Etherscan.

rinkeby.etherscan.io

 

<Test KFT KIP-7>

 

Klaytnscope

Klaytnscope allows you to find data by monitoring network health and statistics of Klaytn as well as profiling blocks and transactions on Klaytn.

baobab.scope.klaytn.com

 

 

'포트폴리오 > Project_3' 카테고리의 다른 글

크롬익스텐션 지갑 개발 (1)  (0) 2022.03.24
P4: NMM X Harmony Wallet-BlockExplorer-txDaemon  (0) 2022.03.11

댓글