# 스캐너 API 개발 가이드

## **소개** <a href="#undefined" id="undefined"></a>

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

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

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

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

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

{% embed url="<https://docs.favorlet.io/favorlet/biz-guide/scanner>" %}

## 엔드 포인트 URL 설정 방법

<figure><img src="https://1707806715-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYtOeQfD0fJSJvvVkh5lk%2Fuploads%2FtEogweeqrM5EqILqwQju%2Fimage.png?alt=media&#x26;token=841db96d-18c6-4238-aac0-34da934a8967" alt=""><figcaption></figcaption></figure>

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

{% embed url="<https://docs.favorlet.io/favorlet/biz-guide/scanner>" %}

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

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

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

{% hint style="warning" %}
`컬렉션 컨트랙트 주소`와 `서비스 엔드 포인트 URL` 둘 중 하나는 반드시 입력되어야 합니다.
{% endhint %}

***

## **흐름**

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

<figure><img src="https://1707806715-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYtOeQfD0fJSJvvVkh5lk%2Fuploads%2FQ4ivCKiCkkd40E5sGMbf%2FGroup%2047%20(1).png?alt=media&#x26;token=612269fa-0107-454d-b3ca-91151ac90423" alt=""><figcaption></figcaption></figure>

1. 페이버렛 사용자가 인증 QR을 생성합니다.
2. 스캐너는 유효한 QR 코드인지 확인하고 설정한 엔드포인트 URL(API 서버)에 지갑 주소와 그 외 정보를 전달합니다.
3. 요청받은 API 서버는 성공과 실패 여부를 스캐너로 응답합니다.

***

## **스펙**

### **Request**

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

**Method**: `POST`

**Header**: `Content-Type:application/json`

**Body**

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

**Parameter Description**

<table><thead><tr><th width="200.33333333333331">key</th><th width="78">type</th><th width="94">required</th><th>description</th></tr></thead><tbody><tr><td><code>walletAddress</code></td><td>string</td><td>true</td><td>QR 인증을 통해 전달받은 지갑주소입니다.</td></tr><tr><td><code>contractAddress</code></td><td>string</td><td>false</td><td>스캐너 설정 화면에서 <code>컬렉션 컨트렉트 주소</code>에 등록한 주소이며 비워둔 경우 데이터가 나오지 않습니다.</td></tr><tr><td><code>tokenId</code></td><td>string</td><td>false</td><td>토큰 홀더 인증시 인증하려는 토큰 아이디.<br>지갑 인증 시에는 사용되지 않습니다.</td></tr><tr><td><code>expireTime</code></td><td>string</td><td>true</td><td>사용자가 생성한 QR코드에 대한 만료 기한이며<br><code>YYYY-MM-DDTHH:mm:ss.sssZ</code> 포맷입니다.</td></tr><tr><td><code>signature</code></td><td>string</td><td>true</td><td><code>walletAddress</code>와 <code>expireTime</code>이 변조되지 않았는지 검증하기 위한 데이터입니다.<br>값 사용에 대한 여부는 선택 사항이며 자세한 설명은 아래를 참조해주세요.</td></tr></tbody></table>

{% hint style="info" %}
`signature`란?

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

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

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

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

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

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

### **Response**

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

**Header**: `Content-Type:application/json`

* **Case1: success**

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

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

**Screenshot**

<figure><img src="https://1707806715-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FYtOeQfD0fJSJvvVkh5lk%2Fuploads%2Fo3HmGwIB3qkkZmqniYCF%2Fimage.png?alt=media&#x26;token=033c2901-6dbc-4650-b202-36b9f649ef65" alt=""><figcaption><p>(좌) success / (우) fail</p></figcaption></figure>

**Parameter Description**

<table><thead><tr><th width="136">key</th><th width="100.33333333333331">type</th><th>description</th></tr></thead><tbody><tr><td><code>success</code></td><td>boolean</td><td>요청에 대한 성공 여부이며 <code>true</code>또는 <code>false</code>를 반환해야 합니다.</td></tr><tr><td><code>title</code></td><td>string</td><td>스캐너에서 요청 결과 팝업 제목에 표시되는 텍스트입니다.</td></tr><tr><td><code>reason</code></td><td>string</td><td>스캐너에서 요청 결과 팝업 설명에 표시되는 텍스트입니다.</td></tr></tbody></table>

{% hint style="info" %}
응답 결과는 JSON 형태로 응답해야 하며 Response status code는 스캐너에서 참조하지 않습니다.
{% endhint %}

## **API 서버 예제 (Node.js)**

**1. Node.js 설치**

{% embed url="<https://nodejs.org/ko/download/>" %}

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

```shell
mkdir api_endpoint_example
cd api_endpoint_example
```

**3. 패키지 설치**

```shell
npm install express ethers
```

**4.** `qrAuth.js` **파일 작성**

```javascript
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` **파일 작성**

```javascript
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. 터미널에서 실행**

```shell
node index.js
```

**7. 새 터미널에서 테스트**

* 테스트 (ethereum)

```shell
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. 결과 확인**

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

{% hint style="info" %}
만약 스캐너앱의 기본 기능 외에 다른 액션을 추가하고 싶으신데 개발에 어려움을 겪고 있다면 <mark style="background-color:red;"><favorlet@fingerlabs.io></mark> 로 이메일 주시길 바랍니다.
{% endhint %}
