스캐너 API 개발 가이드

NFT 홀더 인증 이후 추가적인 기능 구현을 위해 준비된 개발 가이드

소개

본 문서는 NFT 홀더 인증 이후 추가적인 기능 구현을 위해 준비된 개발 가이드입니다.

블록체인과 관련된 기능을 구현하기 위해선 web3.js, ethers 등의 SDK에 대한 이해가 요구될 수 있습니다.

이 문서를 참고하여 API 엔드포인트 서버를 개발한다면 아래 예시의 기능을 추가할 수 있습니다.

지갑을 인증한 사용자가 몇 개의 NFT를 가지고 있는지 확인 스캐너에 인식된 사용자에게 NFT 또는 토큰 전송 페이버렛과 페이버렛 스캐너를 활용한 오프라인 스탬프 투어 오프라인 홀더 파티에서의 출입 체크 및 기록 관리 그 외 상황에 맞는 다양한 기능들

페이버렛 스캐너의 기본적인 이용 가이드는 홀더 인증 (with 스캐너)를 참조해주세요.

엔드 포인트 URL 설정 방법

페이버렛 스캐너 기본 설정 방법은 홀더 인증 (with 스캐너)를 참조해주세요.

스캐너 설정 화면에서 서비스 엔드 포인트 URL에 넣어 사용자의 인증 QR 스캔 이후 등록한 API 서버로 요청 받을 수 있습니다.

스캐너 설정 화면에서 다음 세 가지 방법으로 설정할 수 있습니다.

  1. 컬렉션 컨트랙트 주소만 넣은 경우: 스캐너의 NFT 홀더 인증만 거칩니다.

  2. 서비스 엔드 포인트 URL만 넣은 경우: 스캐너의 NFT 홀더 인증을 건너뛰고 API 서버로 요청을 보냅니다.

  3. 컬렉션 컨트랙트 주소서비스 엔드 포인트 URL 둘 다 넣은 경우: 스캐너의 NFT 홀더 인증 후 API 서버로 요청을 보냅니다.

컬렉션 컨트랙트 주소서비스 엔드 포인트 URL 둘 중 하나는 반드시 입력되어야 합니다.


흐름

페이버렛과 스캐너, 서버가 통신을 하는 흐름은 다음과 같습니다.

  1. 페이버렛 사용자가 인증 QR을 생성합니다.

  2. 스캐너는 유효한 QR 코드인지 확인하고 설정한 엔드포인트 URL(API 서버)에 지갑 주소와 그 외 정보를 전달합니다.

  3. 요청받은 API 서버는 성공과 실패 여부를 스캐너로 응답합니다.


스펙

Request

FAVORLET 스캐너가 서비스 엔드 포인트 URL로 설정한 API 서버에게 요청 보내는 데이터입니다.

Method: POST

Header: Content-Type:application/json

Body

{
    "walletAddress": "0xd464B499639A267Da03721b2DBa7469896732947",
    "contractAddress": "0x8F5Aa6b6DCD2D952A22920E8fE3f798471D05901", // 토큰 홀더 인증을 설정한 경우
    "tokenId": "1", // 토큰 홀더 인증을 설정한 경우
    "expireTime": "2023-12-06T02:44:57.000Z",
    "signature": "0x5b2fe8961ea45988d9eb6f1f927bd06fbd5b495de240b88f4d76cee293cc3a1b56c616c0046bcd73874c24555c081d4f49c58ff25bcb6233ec50018d8efc5ce21b"
}

Parameter Description

keytyperequireddescription

walletAddress

string

true

QR 인증을 통해 전달받은 지갑주소입니다.

contractAddress

string

false

스캐너 설정 화면에서 컬렉션 컨트렉트 주소에 등록한 주소이며 비워둔 경우 데이터가 나오지 않습니다.

tokenId

string

false

토큰 홀더 인증시 인증하려는 토큰 아이디. 지갑 인증 시에는 사용되지 않습니다.

expireTime

string

true

사용자가 생성한 QR코드에 대한 만료 기한이며 YYYY-MM-DDTHH:mm:ss.sssZ 포맷입니다.

signature

string

true

walletAddressexpireTime이 변조되지 않았는지 검증하기 위한 데이터입니다. 값 사용에 대한 여부는 선택 사항이며 자세한 설명은 아래를 참조해주세요.

signature란?

블록체인에서 문자열에 대해 지갑의 private key로 서명하여 나오는 데이터로, 이 signature 를 다시 복구했을 때 서명한 지갑 주소가 나온다면 해당 지갑으로 서명한 데이터임을 검증할 수 있습니다.

Favorlet에서 QR 인증 시 "i-love-fingerlabs_" + walletAddress + "_" + expireTime 텍스트로 서명하게 되며, 이를 통해 walletAddressexpireTime 값을 검증할 수 있습니다.

메세지 서명을 할 때 체인에 따라 prefix가 달라지게 되며 스캐너에서는 이더리움 prefix를 고정으로 사용하고 있습니다.

  • 이더리움 prefix:"\x19Ethereum Signed Message:\n" + message.length + message

자바스크립트에서는 ethersweb3.js 라이브러리를 사용하여 메세지를 복구 할 수 있습니다.

메세지 서명과 관련된 자세한 내용을 알고 싶다면 https://info.etherscan.com/verify-signature-tool/ 를 참조 바라며, 검증에 대한 방법은 본 문서 아래의 서버 예제를 참고 바랍니다.

Response

서비스 엔드 포인트 URL로 설정한 API 서버가 FAVORLET 스캐너로 응답해야하는 데이터입니다.

Header: Content-Type:application/json

  • Case1: success

    {
        "success": true,
        "title": "Success! 😀",
        "reason": "Authentication succeeded."
    }
  • Case2: fail

    {
        "success": false,
        "title": "Fail! 😥",
        "reason": "Authentication failed."
    }

Screenshot

Parameter Description

keytypedescription

success

boolean

요청에 대한 성공 여부이며 true또는 false를 반환해야 합니다.

title

string

스캐너에서 요청 결과 팝업 제목에 표시되는 텍스트입니다.

reason

string

스캐너에서 요청 결과 팝업 설명에 표시되는 텍스트입니다.

응답 결과는 JSON 형태로 응답해야 하며 Response status code는 스캐너에서 참조하지 않습니다.

API 서버 예제 (Node.js)

1. Node.js 설치

2. 프로젝트 폴더 생성 및 이동

mkdir api_endpoint_example
cd api_endpoint_example

3. 패키지 설치

npm install express ethers

4. qrAuth.js 파일 작성

const ethers = require('ethers');

// QR코드 검증 함수
const qrAuth = (walletAddress, contractAddress, expireTime, signature) => {
  // 값 확인
  if (!walletAddress || !expireTime || !signature) {
    throw new Error('Parameter error.');
  }

  // walletAddress, contractAddress 값 검증
  if (!ethers.isAddress(walletAddress)) {
    throw new Error('Invalid wallet address.');
  }

  if (contractAddress && !ethers.isAddress(contractAddress)) {
    throw new Error('Invalid contract address.');
  }

  // QR 만료 시간 확인
  if (new Date(expireTime) < new Date()) {
    throw new Error('QR Expired.');
  }

  const loweredCasedWalletAddress = walletAddress.toLowerCase();
  // recover할 메세지
  const message = `i-love-fingerlabs_${loweredCasedWalletAddress}_${expireTime}`;

  // 메세지에 대해 Byte Array 타입으로 변경
  const msgHash = ethers.hashMessage(message);
  const msgHashBytes = ethers.getBytes(msgHash);
  const recoverAddress = ethers.recoverAddress(msgHashBytes, signature);

  // recover한 주소와 walletAddress 비교
  if (recoverAddress.toLowerCase() !== loweredCasedWalletAddress) {
    throw new Error('Validation Failed.');
  }
};

module.exports = qrAuth;

5. index.js 파일 작성

const express = require('express');
const ethers = require('ethers');

const app = express();
app.use(express.json());

// prefixChain: Klaytn Or Ethereum (Klaytn Chain 외 다른 체인은 전부 Ethereum Prefix)
const hashMessage = ({ prefixChain, message }) => {
  const messagePrefix = `\x19${prefixChain} Signed Message:\n`;

  if (typeof(message) === 'string') {
    message = ethers.utils.toUtf8Bytes(message);
  }

  return ethers.utils.keccak256(ethers.utils.concat([
    ethers.utils.toUtf8Bytes(messagePrefix),
    ethers.utils.toUtf8Bytes(String(message.length)),
    message
  ]));
}

// signature 인증
const qrVerify = ({ prefixChain, walletAddress, signature, expireTime }) => {

  // recover 할 메시지 원문
  const message = `i-love-fingerlabs_${walletAddress}_${expireTime}`;
  // 원문을 hash
  const hashedMessage = hashMessage({ prefixChain, message });
  
  const recoverAddress = ethers.utils.recoverAddress(
    hashedMessage,
    signature
  );

  // recover한 주소와 walletAddress 비교, QR 만료 시간 확인
  if (recoverAddress !== walletAddress || new Date(expireTime) < new Date()) {
    throw new Error('Validation Failed.');
  }
};

// 요청받을 API 엔드포인트
app.post('/', (req, res) => {
  const { prefixChain, walletAddress, signature, expireTime } = req.body;

  qrVerify({ 
    prefixChain,
    walletAddress,
    signature,
    expireTime
  });

  /**
   * 작업할 코드
   * ex) NFT mint, Token airdrop, Holder pass...
   */

  // 성공
  res.send({
    success: true,
    title: "Success! 😀",
    reason: "Authentication succeeded."
  });
});

// 에러 핸들러
app.use((err, req, res, next) => {
  // 실패
  res.send({
    success: false,
    title: "Fail! 😥",
    reason: "Authentication failed."
  });
});

// 3000번 포트로 listen
app.listen(3000, () => {
  console.log('running => http://localhost:3000');
});

6. 터미널에서 실행

node index.js

7. 새 터미널에서 테스트

  • 테스트 (ethereum)

curl --location --request POST 'http://localhost:3000' \
--header 'Content-Type: application/json' \
--data-raw '{
  "walletAddress": "0x94a8acc5c8b33f02ef0b7054ed51b9475ab33742",
  "expireTime": "2023-12-06T03:08:45.000Z",
  "signature": "0xaca28ee707213f1dae11c37c96b3fdf811cca2e5c09623298bbd4483095db0dc3e70c63e10999761cdc433b8f6f82ca6d65dd7a9a84d89cfdca058aecee942981b"
}'

8. 결과 확인

{
  "success":true,
  "title":"Success! 😀",
  "reason":"Authentication succeeded."
}

만약 스캐너앱의 기본 기능 외에 다른 액션을 추가하고 싶으신데 개발에 어려움을 겪고 있다면 favorlet@fingerlabs.io 로 이메일 주시길 바랍니다.

Last updated