명품 영수증 관리 애플리케이션

개요

고가의 명품을 중고 거래할 때 명품을 구입한 가게에서 받은 영수증도 같이 보여주는 일은 꽤 빈번합니다. 내가 산 명품이 "진짜"임을 증명하는 방법이기도 하고, 모조품을 구하는 것보다 가짜 영수증을 만드는 것이 어떻게보면 더 어렵기 때문입니다. 그런데, 이렇게 중고 거래를 하려면 처음 받은 영수증을 보관하고 있어야 하는데 영수증은 보관하기 어렵습니다. 작고 얇아서 잃어버리거나 찢어지거나 구거지기 쉽고, 감열지를 사용하므로 열이나 빛을 받으면 내용이 쉽게 지워집니다.

따라서 블록체인에 명품 영수증을 보관한다면 영수증을 안전하게 영구적으로 보관하면서 이를 거래 시 자유롭게 활용할 수 있을 것입니다. 이 튜토리얼은 블록체인인 클레이튼상에서 영수증을 보관하고 전송하는 BApp(블록체인 애플리케이션) 예제를 소개합니다. 이 예제는 클레이튼의 대체 불가 토큰(Non-fungible Token, NFT)인 KIP-17을 기반으로 전자 영수증을 발행해서 중고 거래에 사용하도록 합니다. 영수증은 클레이튼에 배포되므로 영수증 주인이 개인키를 잃어버리지 않는 한 클레이튼상에 계속 보관해둘 수 있습니다. 따라서 중고 거래를 할 때 새로운 주인에게 영수증을 안전하고 쉽게 전달할 수 있고, 새로운 주인이 명품을 재판매할 때에도 동일하게 다음 주인에게 영수증을 넘겨줄 수 있습니다.

하나의 명품은 하나의 NFT(KIP-17) 스마트 컨트랙트를, 하나의 영수증은 하나의 NFT 토큰을 나타냅니다. 명품 브랜드마다, 혹은 브랜드를 판매하는 가게 수준에서 관리하는 기능을 추가할 수도 있으나, 이 튜토리얼에서는 하나의 가게만 존재하는 상황을 가정합니다.

이 애플리케이션의 핵심 기능은 아래와 같습니다.

  1. 새로운 NFT 스마트 컨트랙트 배포 = 명품 가게에서 새로운 명품을 (블록체인상에서) 등록

  2. 새로운 NFT 토큰 발행 = 가게에서 명품을 판매하고 전자 영수증을 (블록체인상에서) 손님에게 발급

  3. NFT 토큰 전송 = 가게에서 명품을 구매한 손님이 다른 사람에게 중고로 판매할 때 (블록체인상에서) 영수증을 전송

위 기능들을 구현하기 위해, 아래와 같이 시스템을 구성하였습니다.

  • Frontend: React

  • Backend: Node.js, caver-js

    • caver-js: KIP-17 토큰 컨트랙트 배포, 토큰 발행, 토큰 전송

  • DB: MySQL

    • 명품 가게에서 상품 정보, 발행한 영수증 정보 저장

  • KAS: Node API, Token History API

    • Node API로 클레이튼 Endpoint Node 사용

    • Token History API로 토큰 발급 기록 조회

이 페이지의 모든 내용은 KAS로 블록체인 애플리케이션 개발을 돕는 예제입니다. 소스코드를 포함한 모든 내용은 사용자 개발 환경에 따라 다르게 적용될 수 있습니다. 소스코드를 포함한 모든 내용을 사용하는 것에 대한 모든 책임은 전적으로 사용자 본인에게 있습니다.

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

시작하기

프로그램 설치

  • Node.js & yarn 설치

  • MySQL 설치

    • 코드에서는 아이디 root, 패스워드 root로 설정되어있습니다.

    • test 데이터베이스에 테이블이 존재하기 때문에, test 데이터베이스가 없는 경우 생성해야 합니다.

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

데이터베이스 셋업

  • productreceipt 두 개의 테이블이 필요합니다.

  • product는 등록된 명품을 관리하기 위한 테이블입니다.

CREATE TABLE `product` (
  `id` INT NOT NULL AUTO_INCREMENT,
    `name` VARCHAR(64) NOT NULL,
    `image` VARCHAR(128) NOT NULL,
    `contractAddr` VARCHAR(256) DEFAULT NULL,
    `registeredDate` DATETIME NOT NULL,
    `isDeleted` INT NOT NULL,
    `symbol` VARCHAR(64) NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE = INNODB AUTO_INCREMENT = 22 DEFAULT CHARSET = utf8;
  • receipt 는 발행된 영수증을 관리하기 위한 테이블입니다.

CREATE TABLE `receipt` (
  `id` INT NOT NULL AUTO_INCREMENT,
    `sellerID` INT NOT NULL,
    `productID` INT NOT NULL,
    `tokenID` BIGINT NOT NULL,
    `tokenURI` VARCHAR(256) NOT NULL,
    `contractAddr` VARCHAR(256) NOT NULL,
    `fromAddr` VARCHAR(256) NOT NULL,
    `toAddr` VARCHAR(256) NOT NULL,
    `registeredDate` DATETIME NOT NULL,
    `lastUpdatedDate` DATETIME NOT NULL,
    `isDeleted` INT NOT NULL,
    PRIMARY KEY (`id`)
) ENGINE = INNODB AUTO_INCREMENT = 29 DEFAULT CHARSET = utf8;

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

저장소 복사하기

git clone git@github.com:ground-x/kas-bapp-luxurytracker.git

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

npm install

  • 루트 디렉토리의 server.js 파일이 백엔드 서버 기능을 담당하고, client 디렉토리는 리액트 기반 프론트엔드 기능을 담당합니다. 이렇게, 이 튜토리얼은 백엔드/프론트엔드 모두 자바스크립트 기반의 패키지 매니저를 사용했기 때문에 npm install이 2번 필요합니다: 루트 디렉토리에서 1번, client 디렉토리에서 1번 npm install이 필요합니다.

npm install
cd client
npm install

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

KAS 설정하기

  • client/src/Config.js에 KAS Console을 통해 발급받은 accessKeyId와 secretAccessKey를 입력합니다.

const accessKeyId = "YOUR_ACCESS_KEY_ID_FROM_KAS_CONSOLE";
const secretAccessKey = "YOUR_SECRET_ACCESS_KEY_FROM_KAS_CONSOLE";

튜토리얼에 사용할 계정 충전하기

  • 본 튜토리얼에 사용되는 판매자와 구매자 계정은 트랜잭션을 직접 발생시키기 때문에, 트랜잭션 발생에 필요한 KLAY가 없다면 node.js에서 아래와 같은 에러가 발생할 수 있습니다. 테스트넷인 Baobab 네트워크에서 본 튜토리얼을 실행하는 경우 Faucet을 통해서 테스트에 필요한 KLAY를 지급 받을 수 있습니다. Cypress 네트워크를 사용할 경우 판매자와 구매자 계정에 KLAY를 직접 충전해주어야 합니다.

[0] (node:115456) UnhandledPromiseRejectionWarning: Error: Returned error: insufficient funds of the sender for value

실행하기

  • 루트 디렉토리에서 아래 명령어를 실행합니다. 아래 명령어로 백엔드 서버와 프론트엔드를 한 번에 실행시킬 수 있습니다. http://localhost:3000 에서 실행된 프로그램을 확인할 수 있습니다.

    (해당 포트가 이미 사용중인 경우 다른 포트 번호를 사용하십시오.)

yarn dev

사용 영상

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

애플리케이션 워크플로우

위에서 소개한 것처럼 이 예제 BApp이 수행하는 핵심 기능은 아래와 같습니다.

  1. 새로운 NFT 스마트 컨트랙트 배포 = 명품 가게에서 새로운 명품을 (블록체인상에서) 등록

  2. 새로운 NFT 토큰 발행 = 가게에서 명품을 판매하고 전자 영수증을 (블록체인상에서) 손님에게 발급

  3. NFT 토큰 전송 = 가게에서 명품을 구매한 손님이 다른 사람에게 중고로 판매할 때 (블록체인상에서) 영수증을 전송

프론트엔드에서는 애플리케이션 사용자(=명품 가게 주인, 명품을 구입하려는 손님)의 요청을 백엔드 서버로 보내고, 백엔드 서버의 응답을 사용자에게 보여줍니다. 프론트엔드 코드는 저장소에 있는 코드를 확인하십시오.

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

새 명품을 가게에서 파는 상품 목록에 등록

새로운 명품을 등록하는, 즉 새로운 컨트랙트를 배포하는 코드입니다. 이 튜토리얼에서는 명품 가게 주인이 새롭게 판매할 명품을 블록체인상에 직접 등록합니다. 명품 가게 주인이 새로운 스마트 컨트랙트를 배포 caver-js를 이용해서 KIP-17 컨트랙트를 배포하기 위해서는 가게 주인의 개인키가 필요합니다. 이 예제에서는 간단한 구현을 위해 클라이언트에서 서버에 보내는 req 오브젝트에 가게 주인 개인키가 들어 있습니다. 실제 서비스 개발시에는 서버에서 가게 주인의 개인키를 보관하는 것이 좋습니다.

./client/src/components/ProductAdd.js에서 KIP-17 컨트랙트를 클레이튼에 배포하겠다는 요청을 보내면, ./server.js 에서 이 요청을 받아서

  1. 새로운 KIP-17 컨트랙트(=새롭게 판매할 명품)를 배포(=등록)하고

  2. 배포된 KIP-17 컨트랙트의 정보를 product 테이블에 저장합니다.

전체 플로우는 다음과 같습니다.

  1. 가게 주인 개인키를 caver 지갑에 등록 (이미 등록된 경우 생략) - 프론트엔드

  2. 등록할 명품 정보 입력, 명품 정보를 백엔드 서버에 전송 - 프론트엔드

  3. 프론트엔드에서 받은 명품 정보를 바탕으로 KIP-17 컨트랙트 배포 - 백엔드

  4. 배포된 KIP-17 컨트랙트에 대한 정보(=새 명품 정보)를 product 테이블에 저장 - 백엔드

/////////////////
// ./server.js //
/////////////////
// 컨트랙트(=명품) 배포 코드
app.post("/api/products", upload.single("image"), async (req, res) => {
  // 프론트엔드에서 백엔드 서버로 넘어온 데이터를 확인하는 코드입니다.
  console.log(
    "name", req.body.name,
    "symbol", req.body.symbol,
    "sellerPrivateKey", req.body.sellerPrivateKey
  );

  // 개인키를 바탕으로 keyring을 생성합니다.
  // keyring에 대한 자세한 내용은 https://ko.docs.klaytn.com/bapp/sdk/caver-js/api-references/caver.wallet/keyring 를 참고하세요.
  const keyring = caver.wallet.keyring.createFromPrivateKey(
    req.body.sellerPrivateKey
  );

  // wallet에 keyring이 추가되지 않은 경우에만 keyring을 추가합니다.
  if (!caver.wallet.getKeyring(keyring.address)) {
    const singleKeyRing = caver.wallet.keyring.createFromPrivateKey(
      req.body.sellerPrivateKey
    );
    caver.wallet.add(singleKeyRing);
  }

  // 넘어온 데이터를 바탕으로 새로운 KIP-17을 배포(=새로운 명품 등록)합니다. 
  const kip17 = await caver.kct.kip17.deploy(
    {
      name: req.body.name,
      symbol: req.body.symbol,
    },
    keyring.address
  );

  // 배포된 KIP-17의 컨트랙트 주소를 확인합니다.
  console.log(KIP-17.options.address);

  // 배포된 KIP-17의 정보를 `TEST.PRODUCT` 테이블에 삽입합니다.
  let sql =
    "INSERT INTO TEST.PRODUCT (name, symbol, contractAddr, image, registeredDate, isDeleted) VALUES (?, ?, ?, ?, now(), 0)";
  console.log(req.file);
  let image = "/image/" + req.file.filename;
  let name = req.body.name;
  let symbol = req.body.symbol;
  let params = [name, symbol, KIP-17.options.address, image];

  connection.query(sql, params, (err, rows, fields) => {
    res.send(rows);
    console.log(err);
  });

  console.log("end of /api/products post");
});

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

명품 매출 발생 시 손님에게 영수증 발행

새로운 스마트 컨트랙트를 배포함으로써 새롭게 출시되는 명품을 블록체인에 등록헸다면, 명품 가게 주인은 이제 이 명품을 판매하기 시작할 것입니다. 명품 가게에 찾아온 손님에게 명품을 판매하면 손님에게 영수증을 발급합니다. 이 때 전자 영수증을 블록체인상에서 손님에게 발급합니다.

블록체인상에서 영수증을 손님에게 발급하는 것은, 위에서 만든 KIP-17 스마트 컨트랙트에서 새로운 토큰을 손님의 EOA로 발행하는 것과 같습니다. caver-js를 이용해서 KIP-17 토큰을 발급하기 위해서는 가게 주인의 개인키가 필요합니다. 이 예제에서는 간단한 구현을 위해 클라이언트에서 서버에 보내는 req 오브젝트에 가게 주인 개인키가 들어 있습니다. 실제 서비스 개발시에는 서버에서 가게 주인의 개인키를 보관하는 것이 좋습니다.

./client/src/components/ReceiptAdd.js 에서 요청을 보내면, ./server.js 에서 요청을 받아서

  1. 새로운 KIP-17 토큰(=전자 영수증)을 손님에게 발급하고

  2. 발급한 KIP-17 토큰의 정보를 receipt 테이블에 저장합니다.

전체 플로우는 다음과 같습니다.

  1. 가게 주인 개인키를 caver 지갑에 등록 (이미 등록된 경우 생략) - 프론트엔드

  2. 발행할 영수증 정보 입력, 백엔드 서버에 이 영수증 정보를 전송 - 프론트엔드

  3. 프론트엔드에서 받은 영수증 정보를 바탕으로 새로운 KIP-17 토큰 발급 - 백엔드

  4. 발급된 KIP-17 토큰에 대한 정보를 receipt 테이블에 저장 - 백엔드

/////////////////
// ./server.js //
/////////////////
/* 
토큰(=영수증) 발행 코드:
fromAddr는 명품 가게 주인의 클레이튼 EOA 주소,
toAddr는 명품을 구입하려는 손님의 클레이튼 EOA 주소,
tokenURI는 토큰 정보,
contractAddr는 새롭게 판매할 명품을 블록체인상에 등록하기 위한 스마트 컨트랙트 주소입니다.
*/
app.post("/api/receipts", async (req, res) => {
  // 프론트엔드에서 백엔드 서버로 넘어온 데이터를 확인하는 코드입니다.
  console.log("req.body.sellerPrivateKey: " + req.body.sellerPrivateKey);
  console.log("req.body.productID: " + req.body.productID);
  console.log("req.body.toAddr: " + req.body.toAddr);

  // 개인키를 바탕으로 keyring을 생성합니다.
  // keyring에 대한 자세한 내용은 https://ko.docs.klaytn.com/bapp/sdk/caver-js/api-references/caver.wallet/keyring 를 참고하세요.
  const keyring = caver.wallet.keyring.createFromPrivateKey(
    req.body.sellerPrivateKey
  );

  // wallet에 keyring이 추가되지 않은 경우에만 keyring을 추가합니다.
  if (!caver.wallet.getKeyring(keyring.address)) {
    const singleKeyRing = caver.wallet.keyring.createFromPrivateKey(
      req.body.sellerPrivateKey
    );
    caver.wallet.add(singleKeyRing);
  }

  // 넘어온 productID와 일치하는 명품이 등록되어 있는지 조회합니다.
  connection.query(
    "SELECT * FROM TEST.PRODUCT WHERE isDeleted = 0 AND id = ?",
    req.body.productID,
    async (err, rows, fields) => {
      // product.id는 primary key이기 때문에 하나의 row만 리턴해야합니다.
      console.log("keyring.address", keyring.address);
      console.log("contractAddr", rows[0]["contractAddr"]);

      // 넘어온 productID에 대응되는 컨트랙트 주소입니다.
      contractAddr = rows[0]["contractAddr"];

      // 컨트랙트 주소 기반으로 KIP-17 오브젝트를 생성합니다.
      const kip17 = new caver.kct.kip17(contractAddr);

      // 새로 발행하는 토큰에 임의의 tokenId를 할당하기 위해 Math.random 사용 및 중복 여부를 체크합니다.
      minted = false;
      while (true) {
        randomTokenID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
        console.log("randomTokenID", randomTokenID);
        try {
          owner = await KIP-17.ownerOf(randomTokenID);
        } catch (e) {
          // owner가 존재하지 않는 경우(=존재하지 않는 tokenID) 에러가 리턴됩니다.
          // 에러를 받으면 해당 tokenID로 토큰 생성이 가능합니다.
          console.log("we can mint");

          // tokenURI에는 임의의 정보를 넣어줄 수 있습니다.
          // 본 예제에서는 임의의 sellerID와 productID를 json 형태로 저장합니다.
          // 토큰 이미지 URL이나 기타 정보를 tokenURI에 저장할 수 있습니다.
          tokenURI = JSON.stringify({
            sellerID: 0,
            productID: req.body.productID,
          });

          // KIP-17.mintWithTokenURI를 이용해서 새로운 토큰을 발행합니다.
          // 자세한 내용은 https://ko.docs.klaytn.com/bapp/sdk/caver-js/api-references/caver.kct/KIP-17#KIP-17-mintwithtokenuri 를 참고하세요.
          mintResult = await KIP-17.mintWithTokenURI(
            req.body.toAddr,
            randomTokenID,
            tokenURI,
            { from: keyring.address }
          );

          // 클레이튼 네트워크에 토큰이 발행된 다음, 생성된 토큰을 `receipt` 데이터베이스에 저장합니다.
          let sql =
            "INSERT INTO TEST.RECEIPT " +
            "(sellerID, productID, tokenID, tokenURI, contractAddr, fromAddr, toAddr, registeredDate, lastUpdatedDate, isDeleted)" +
            " VALUES (?, ?, ?, ?, ?, ?, ?, NOW(), NOW(), 0)";

          let params = [
            0,
            req.body.productID,
            randomTokenID,
            tokenURI,
            contractAddr,
            "0x0000000000000000000000000000000000000000",
            req.body.toAddr,
          ];

          connection.query(sql, params, (err, rows, fields) => {
            if (!err) {
              console.log("error while inserting a new receipt", "err", err);
            }
          });
          minted = true;
        }
                // 발급된 경우, 즉 randomTokenID를 갖는 토큰이 존재하지 않아서 새로운 토큰이 발급된 경우에만 break 합니다.
        // 발급되지 않은 경우에는 새로운 randomTokenID를 바탕으로 재시도합니다.
        if (minted) {
          break;
        }
      }
      res.send(rows);
    }
  );
  console.log("end of /api/products post");
});

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

중고품 판매자가 구매자에게 가게에서 받은 영수증 전송

손님은 구매한 명품을 한동안 잘 사용하다가 이를 중고로 처분하고 싶은 마음이 생겼습니다. 이렇게 명품을 다른 사람에게 중고로 판매할 때, 명품 가게에서 받은 영수증도 중고품 구매자에게 함께 전달하려고 합니다. 이 예제 BApp에서는 중고 명품 판매자가 중고 명품 구매자에게 KIP-17 토큰을 보냄으로써, 명품을 처음 살 때 받은 영수증을 다른 사람에게 넘깁니다.

이 예제에서는 간단한 구현을 위해 클라이언트에서 서버에 보내는 req 오브젝트에 중고 명품 판매자의 개인키가 들어 있습니다. 실제 서비스 개발시에는 서버에서 중고 명품 판매자의 개인키를 보관하는 것이 좋습니다.

./client/src/components/ReceiptSend.js 에서 요청을 보내면, ./server.js 에서 요청을 받아서

  1. 중고 명품 판매자가 KIP-17 토큰(=영수증)을 중고 명품 구매자에게 전송하고

  2. 전송한 KIP-17 토큰의 정보를 receipt 테이블에 업데이트합니다.

전체 플로우는 다음과 같습니다.

  1. 중고 명품 판매자의 개인키를 caver 지갑에 등록 (이미 등록된 경우 생략) - 프론트엔드

  2. 중고 명품 판매자가 구매자에게 전송할 영수증을 선택 후 중고 명품 구매자의 EOA 주소를 입력하면, 이 EOA를 백엔드 서버에 전송 - 프론트엔드

  3. 프론트엔드에서 받은 정보를 바탕으로 KIP-17 토큰(영수증)을 중고 명품 구매자 EOA로 전송 - 백엔드

  4. 전송한 KIP-17 토큰에 대한 정보를 receipt 테이블에 업데이트 - 백엔드

영수증 전송 과정을 좀 더 자세히 설명하면 아래와 같습니다.

  • 중고 명품 판매자는 BApp에서 자신의 중고 명품 구매자의 클레이튼 계정 주소(=EOA)를 선택하고 판매 버튼을 클릭

  • ReceiptSend.js는 영수증을 중고 명품 판매자로부터 구매자에게 전송하라고 백엔드 서버에 요청

  • 백엔드 서버(server.js)는 이 요청을 받고, 먼저 영수증 소유권이 변경되었다는 내용을 DB에 업데이트

  • DB 업데이트 후, server.js는 영수증을 판매자로부터 구매자에게 전송하는 트랜잭션을 KAS에 요청

  • KAS가 이 트랜잭션을 실행한 후, 중고 명품 구매자는 자신의 BApp에서 명품 영수증을 전달 받았다는 메세지 수신

  • 구매자는 Klaytnscope에서 자신이 영수증을 잘 전달 받았는지(블록체인상에서 영수증 소유권이 잘 변경되었는지) 확인 가능

  • 구매자는 BApp상에서도 언제든지 전달받은 영수증과 소유권 정보를 확인 가능

/////////////////
// ./server.js //
/////////////////
// 토큰(=영수증) 전송 코드
app.post("/api/receipts/send", async (req, res) => {
  // 프론트엔드에서 백엔드 서버로 넘어온 데이터를 확인하는 코드입니다.
  console.log("post /api/receipts");
  console.log("req.body.customerPrivateKey: " + req.body.customerPrivateKey);
  console.log("req.body.contractAddr: " + req.body.contractAddr);
  console.log("req.body.tokenId: " + req.body.tokenId);
  console.log("receiverAddr", req.body.receiverAddr);

  // 개인키를 바탕으로 keyring을 생성합니다.
  // keyring에 대한 자세한 내용은 https://ko.docs.klaytn.com/bapp/sdk/caver-js/api-references/caver.wallet/keyring 를 참고하세요.
  let senderPrivateKey = req.body.customerPrivateKey;
  const senderKeyring = caver.wallet.keyring.createFromPrivateKey(
    senderPrivateKey
  );

  // wallet에 keyring이 추가되지 않은 경우에만 keyring을 추가합니다.
  if (!caver.wallet.getKeyring(senderKeyring.address)) {
    const singleKeyRing = caver.wallet.keyring.createFromPrivateKey(
      senderPrivateKey
    );
    caver.wallet.add(singleKeyRing);
  }
  let contractAddr = req.body.contractAddr;

  // receipt 테이블에서 fromAddr와 toAddr를 새로운 값으로 업데이트합니다.
  connection.query(
    "UPDATE TEST.RECEIPT SET fromAddr = ?, toAddr = ?, lastUpdatedDate=NOW() WHERE contractAddr=? AND tokenID=?",
    [
      senderKeyring.address,
      req.body.receiverAddr,
      contractAddr,
      req.body.tokenId,
    ],
    // receipt 테이블 업데이트 이후에 KIP-17을 새로운 주인에게 전송합니다.
    async (err, rows, fields) => {
      const kip17 = new caver.kct.kip17(contractAddr);

      console.log(`senderKeyring.address: ${senderKeyring.address}`);
      console.log(`req.body.receiverAddr: ${req.body.receiverAddr}`);
      console.log(`req.body.tokenId: ${typeof req.body.tokenId}`);

      console.log(caver.currentProvider);
      transferResult = await KIP-17.transferFrom(
        senderKeyring.address,
        req.body.receiverAddr,
        req.body.tokenId,
        { from: senderKeyring.address, gas: 200000 }
      );
      console.log(transferResult);
      res.send(transferResult);
    }
  );
});

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

부가 기능: 명품 판매 기록 조회

명품 가게 주인은 자신이 판매한 명품 하나에 대해, 손님에게 발급한 영수증 목록을 불러올 수 있습니다. 이는 곧 어떤 KIP-17 컨트랙트 1개에서 발행한 모든 토큰 목록을 불러오는 것과 같습니다. product 테이블에 저장되어있는 contractAddr 을 바탕으로 Token History API의 NFT 조회 기능을 사용하면 영수증 목록을 불러올 수 있습니다. 이 부분에서는 위 예제 코드들과는 다르게 caver-js 를 사용하지 않고 REST API만을 사용합니다.

전체 플로우는 다음과 같습니다.

  1. 영수증 목록을 조회하고 싶은 명품 정보를 백엔드 서버에 전송 - 프론트엔드

  2. 프론트엔드에게 받은 명품 정보(=명품 아이디)를 바탕으로 product 테이블에서 대응되는 컨트랙트 주소 조회 - 백엔드

  3. 조회한 컨트랙트 주소를 바탕으로 KAS Token History API의 NFT 조회 기능 사용 - 백엔드

  4. 조회된 NFT 토큰 리스트를 프론트엔드로 리턴 - 백엔드

/////////////////
// ./server.js //
/////////////////
// 하나의 명품에 대해 발행된 토큰(=영수증) 조회 코드
app.get("/api/receipts/:id", (req, res) => {
  // `product` 테이블에서 주어진 id에 대응되는 `contractAddr`를 가져옵니다.
  connection.query(
    "SELECT * FROM TEST.PRODUCT WHERE isDeleted = 0 AND id = ?",
    req.params.id,
    async (err, rows, fields) => {
      contractAddr = rows[0]["contractAddr"];

      // KAS Token History API 호출에 필요한 데이터를 생성합니다.
      var options = {
        method: "GET",
        url:
          "https://th-api.klaytnapi.com/v2/kct/nft/" + contractAddr + "/token",
        headers: {
          "x-krn": "krn:1001:th",
          "Content-Type": "application/json",
          Authorization:
          // https://docs.klaytnapi.com/getting-started 에서 확인할 수 있는, let credential = btoa(`${accessKey}:${secretKey}`)); 값을 넣어줍니다. 
            "Basic MjAxMzczYTQ3...",
        },
      };
      request(options, function (error, response) {
        if (error) throw new Error(error);
        console.log(response.body);

        items = response.body["items"];
        if (!items) {
          res.send(response.body);
          return;
        }

        items.map((contract) => {});
      });
    }
  );
});

이 문서 혹은 KAS에 관한 문의는 개발자 포럼을 방문해 도움을 받으십시오.

Last updated