소개
본 문서는 NFT 홀더 인증 이후 추가적인 기능 구현을 위해 준비된 개발 가이드입니다.
블록체인과 관련된 기능을 구현하기 위해선 web3.js, ethers 등의 SDK에 대한 이해가 요구될 수 있습니다.
이 문서를 참고하여 API 엔드포인트 서버를 개발한다면 아래 예시의 기능을 추가할 수 있습니다.
지갑을 인증한 사용자가 몇 개의 NFT를 가지고 있는지 확인
스캐너에 인식된 사용자에게 NFT 또는 토큰 전송
페이버렛과 페이버렛 스캐너를 활용한 오프라인 스탬프 투어
오프라인 홀더 파티에서의 출입 체크 및 기록 관리
그 외 상황에 맞는 다양한 기능들
페이버렛 스캐너의 기본적인 이용 가이드는 홀더 인증 (with 스캐너)를 참조해주세요.
엔드 포인트 URL 설정 방법
페이버렛 스캐너 기본 설정 방법은 홀더 인증 (with 스캐너)를 참조해주세요.
스캐너 설정 화면에서 서비스 엔드 포인트 URL
에 넣어 사용자의 인증 QR 스캔 이후 등록한 API 서버로 요청 받을 수 있습니다.
스캐너 설정 화면에서 다음 세 가지 방법으로 설정할 수 있습니다.
컬렉션 컨트랙트 주소
만 넣은 경우: 스캐너의 NFT 홀더 인증만 거칩니다.
서비스 엔드 포인트 URL
만 넣은 경우: 스캐너의 NFT 홀더 인증을 건너뛰고 API 서버로 요청을 보냅니다.
컬렉션 컨트랙트 주소
와 서비스 엔드 포인트 URL
둘 다 넣은 경우: 스캐너의 NFT 홀더 인증 후 API 서버로 요청을 보냅니다.
컬렉션 컨트랙트 주소
와 서비스 엔드 포인트 URL
둘 중 하나는 반드시 입력되어야 합니다.
흐름
페이버렛과 스캐너, 서버가 통신을 하는 흐름은 다음과 같습니다.
스캐너는 유효한 QR 코드인지 확인하고 설정한 엔드포인트 URL(API 서버)에 지갑 주소와 그 외 정보를 전달합니다.
요청받은 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
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/ 를 참조 바라며, 검증에 대한 방법은 본 문서 아래의 서버 예제를 참고 바랍니다.
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
응답 결과는 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. 터미널에서 실행
7. 새 터미널에서 테스트
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 로 이메일 주시길 바랍니다.