Building a Simple Token Tracker Using Web3 Data APIs

빌더 여러분 안녕하세요! Token 튜토리얼에 오신 것을 환영합니다 🎉

이번 튜토리얼에서는 간단한 Token Tracker를 설계하고 직접 만들어가는 과정을 볼 수 있습니다.

💡

본 튜토리얼은 다음과 같은 순서로 진행됩니다!

Step 1. 요구사항 수립 및 연동할 API 살펴보기

Step 2. 개발 환경 설정하기

Step 3. API 연동하기

Step 4. 예제 코드로 동작 확인하기

그럼 하나씩 진행해볼까요?


Step 1. 요구사항 수립 및 연동할 API 살펴보기

구현할 Token Tracker는 아래와 같이 간단한 기능을 제공하도록 설계했습니다.

  • 기능 1. 계정 주소를 입력하여 해당 계정이 보유하고 있는 Token의 정보를 보여준다.
    • 기능 1-1. 계정 주소를 입력하여 해당 계정이 보유하고 있는 Native Token의 수량을 보여준다.
    • 기능 1-2. 계정 주소를 입력하여 해당 계정이 보유하고 있는 Token 목록을 조회하여 보여준다.
스크린샷 2024-08-30 오후 5.19.11.png
  • 기능 2. Token 목록에서 특정 Token을 클릭하면 해당 Token에 대한 Metadata와 함께 아래 정보를 보여준다.
    • 기능 2-1. Token Contract의 Metadata를 확인할 수 있다.
    • 기능 2-2. 해당 Token이 거래된 이력을 확인할 수 있다.
스크린샷 2024-09-02 오전 11.00.30.png

간단하지만 Token와 관련된 중요한 기능들을 모두 담고 있네요! 이제 Nodit의 Web3 Data API 문서를 참고하여 각 기능을 구현하기 위해 어떤 API를 사용할 수 있는지 확인해볼까요?

기능 1-1. 계정 주소를 입력하여 해당 계정이 보유하고 있는 Native Token의 수량을 보여준다.

이 기능을 구현하기 위해서는 사용자가 Input에 주소를 입력하고 [검색] 버튼을 클릭하면 API를 호출하여 응답으로부터 해당 계정이 보유한 Native Token의 수량을 조회해야 합니다. 아래 그림과 같이 연동할 수 있습니다.

스크린샷 2024-08-30 오후 5.45.37.png

이러한 인터페이스를 제공하는 API로 Web3 Data API의 Token API중 getNativeBalanceByAccount 라는 API를 사용할 수 있겠네요! 아래와 같이 curl을 작성하여 API를 호출해 볼 수 있습니다.

curl --request POST \
     --url https://web3.nodit.io/v1/ethereum/mainnet/native/getNativeBalanceByAccount \
     --header 'X-API-KEY: Input_your_API_Key' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "accountAddress": "Input_your_account_address"
}
'

위 호출에 대한 응답은 다음과 같습니다. wei 단위로 수량을 확인할 수 있습니다.

{
  "ownerAddress": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "balance": "29435021161388550777"
}

기능 1-2. 계정 주소를 입력하여 해당 계정이 보유하고 있는 Token 목록을 조회하여 보여준다.

계정이 보유한 Native Token의 수량 조회와 동일하게 사용자가 Input에 주소를 입력하고 [검색] 버튼을 클릭하면 API를 호출하여 응답으로부터 해당 계정이 보유한 Token의 목록을 조회해야 합니다.

스크린샷 2024-08-30 오후 5.54.28.png

Native Token을 불러오는 로직과 동일하니 Native Token을 조회한 후 Token 목록을 불러올 수 있는 API를 호출해 목록을 불러오면 되겠네요!

Nodit에서는 getTokensOwnedByAccount 라는 API로 해당 기능을 구현할 수 있으며 아래와 같이 curl을 작성해 API를 호출해 볼 수 있습니다.

curl --request POST \
     --url https://web3.nodit.io/v1/ethereum/mainnet/token/getTokensOwnedByAccount \
     --header 'X-API-KEY: Input_your_API_Key' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "accountAddress": "Input_your_account_address"
}
'

위 호출에 대한 응답은 다음과 같습니다. balance의 경우, Wei 단위로 제공되고 있습니다.

{
    "rpp": 20,
    "page": 2,
    "count": 389,
    "items": [
        {
            "ownerAddress": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
            **"balance": "1000000000000000000000000",**
            "contract": {
                **"address": "0x6B1E3c59561d299Cab1eAa18bbD03eE3fFD2A0C7",**
                "deployedTransactionHash": "0x5eacfd6869aac83fd7b74fb1740d24d504daddcf90d61953b47703194a033af1",
                "deployedAt": "2023-10-26T16:49:59.000Z",
                "deployerAddress": "0x788A0E6c06448723D74efDe653E9d567faA262F5",
                "logoUrl": null,
                "type": "ERC20",
                **"name": "Tether",
                "symbol": "USDT",**
                "totalSupply": "1000000001000000000000000000",
                **"decimals": 18**
            }
        },
      ...
    ]
}

기능 2-1. Token Contract의 Metadata를 확인할 수 있다.

사용자가 목록의 Token 중 한 가지를 선택하면 Contract의 Metadata와 거래 이력을 보여주어야 합니다. 아래 다이어그램과 같이 연동할 수 있겠네요.

스크린샷 2024-08-30 오후 6.05.05.png

Nodit에서 제공하는 getTokenContractMetadataByContracts API로 해당 기능을 구현할 수 있으며 contractAddress를 이용해 아래와 같이 API를 호출해보겠습니다.

 curl --request POST \
     --url https://web3.nodit.io/v1/ethereum/mainnet/token/getTokenContractMetadataByContracts \
     --header 'X-API-KEY: Input_your_API_Key' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "contractAddresses": [
    "Input_your_contract_address"
  ]
}
'

위 호출에 대한 응답으로 다음과 같은 값을 받을 수 있습니다. 이 중에서 필요한 값을 선택해 화면을 구현할 수 있습니다.

[
  {
    "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
    "deployedTransactionHash": "0x2f1c5c2b44f771e942a8506148e256f94f1a464babc938ae0690c6e34cd79190",
    "deployedAt": "2017-11-28T00:41:21.000Z",
    "deployerAddress": "0x36928500Bc1dCd7af6a2B4008875CC336b927D57",
    "logoUrl": "https://cdn.luniverse.io/img/crypto-currencies/ethereum/mainnet/0xdac17f958d2ee523a2206206994597c13d831ec7/64x64.png",
    "type": "ERC20",
    "name": "Tether USD",
    "symbol": "USDT",
    "totalSupply": "53981730120396390",
    "decimals": 6
  }
]

기능 2-2. 해당 Token이 거래된 이력을 확인할 수 있다.

그 다음은 특정 Token의 거래 이력에 대한 데이터를 불러올 차례 입니다. 2-1과 동일한 호출 파라미터를 사용하지만 거래 이력에 대한 정보 목록을 조회할 수 있어야 합니다. Nodit에서는 getTokenTransfersByContract API를 제공하고 있으며 해당 API를 이용해 특정 Token의 거래 이력을 조회해보겠습니다.

스크린샷 2024-08-30 오후 6.07.27.png
curl --request POST \
     --url https://web3.nodit.io/v1/ethereum/mainnet/token/getTokenTransfersByContract \
     --header 'X-API-KEY: Input_your_API_Key' \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "contractAddress": "Input_your_contract_address",
}
'

위 호출에 대한 응답으로 다음과 같은 값을 받을 수 있습니다. 이 중에서 필요한 값을 선택해 화면을 구현할 수 있습니다.

{
  "rpp": 20,
  "cursor": "eyJzb3J0IjpbImJsb2NrX3RpbWVzdGFtcDpkZXNjIiwiYmxvY2tfbnVtYmVyOmRlc2MiLCJsb2dfaW5kZXg6ZGVzYyJdLCJzZWFyY2hBZnRlciI6WzE3MjUwMDg4MTkwMDAsMjA2NDAzOTEsMjAzXX0=",
  "items": [
    {
      "from": "0x0802Dd2EDD3f9faD39A9173B4595be819f201d61",
      "to": "0x09DEdfcef409fCaB410D79E2834FAD16554d1B7c",
      "value": "10200000",
      "timestamp": 1725008831,
      "blockNumber": 20640392,
      "transactionHash": "0x50793eedb9083a23d3725df8c31d727b0365e004dccf3f58ad9353c391c62088",
      "logIndex": 240,
      "contract": {
        "address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
        "deployedTransactionHash": "0x2f1c5c2b44f771e942a8506148e256f94f1a464babc938ae0690c6e34cd79190",
        "deployedAt": "2017-11-28T00:41:21.000Z",
        "deployerAddress": "0x36928500Bc1dCd7af6a2B4008875CC336b927D57",
        "logoUrl": "https://cdn.luniverse.io/img/crypto-currencies/ethereum/mainnet/0xdac17f958d2ee523a2206206994597c13d831ec7/64x64.png",
        "type": "ERC20",
        "name": "Tether USD",
        "symbol": "USDT",
        "totalSupply": "53981730120396390",
        "decimals": 6
      }
    },
  ...
  ]
}

Step 2. 개발 환경 설정

간단한 DApp 구현을 위해 아래 개발 환경 구성이 필요합니다. 설치를 위한 도움이 필요하다면 여기를 클릭해주세요.

VS Code Required

소스 코드 편집기로 코드를 손쉽게 작성하고 편집할 수 있습니다.

Node.js Required

자바스크립트 런타임 환경으로 프로그램 실행을 위해 필요합니다.

git Optional

분산 버전 관리 시스템으로 소스 코드를 로컬 환경에 다운로드 받기 위해 필요합니다. 예제 코드를 다운로드 받을 수 있습니다.

이 외에 사용된 프레임워크, 라이브러리는 Step 4. 예제코드의 package.json 파일을 참고해주세요.

Step 3. API 연동

이미 프로젝트 구현에 필요한 API들을 살펴보고 호출 시 응답이 어떻게 오는지 확인하였습니다. 이제 이 API들을 이용해 프론트엔드 코드에 연동해 볼까요?

📘

Nodit API Key를 발급 받으셨나요?

Nodit API를 호출하기 위해서는 Nodit API Key가 필요합니다. 아직 Nodit을 시작하지 않으셨다면 아래 링크된 페이지의 안내에 따라 Nodit 콘솔에 가입하고, 개발 연동을 위한 API Key를 발급받아보세요. 시작은 무료입니다!

👉 Nodit Node Quickstart

우선 Nodit의 API Key와 Protocol, Network를 환경 변수로 설정하여 하나의 파일에서 관리하도록 하였습니다.

// .env

VITE_API_KEY=-K5v1arBZA9ZC-tYoG9rYbwTLCuAHo8a
VITE_PROTOCOL=ethereum
VITE_NETWORK=mainnet

환경 변수 파일에 작성한 값을 불러와 HTTP 통신을 이용해 API를 호출할 인스턴스를 생성한 코드입니다.

// instance.ts

import axios, { AxiosInstance } from "axios";

const apiKey = import.meta.env.VITE_API_KEY;
const protocol = import.meta.env.VITE_PROTOCOL;
const network = import.meta.env.VITE_NETWORK;

export function createWeb3ApiInstance(): AxiosInstance {
  const instance = axios.create({
    baseURL: `https://web3.nodit.io/v1/${protocol}/${network}`,
    headers: {
      "X-API-KEY": `${apiKey}`,
      "Content-Type": "application/json",
      Accept: "application/json",
    },
  });
  return instance;
}

앞서 작성한 인스턴스를 이용해 API를 호출하는 함수를 만든 코드입니다.

// useQueries.ts

import { useQuery } from "@tanstack/react-query";
import { createWeb3ApiInstance } from "./instance";
const instance = createWeb3ApiInstance();

export const useGetTokensOwnedByAccount = (
  accountAddress: string,
  page: number,
  rpp = 20
) => {
  return useQuery({
    queryKey: ["getTokensOwnedByAccount", accountAddress, page, rpp],
    queryFn: async () =>
      instance.post("token/getTokensOwnedByAccount", {
        accountAddress,
        withCount: true,
        rpp,
        page,
      }),
    retry: false,
    staleTime: 1000 * 60 * 5,
  });
};

화면에 노출될 테이블 컴포넌트를 구성합니다. 부모 컴포넌트에서 처리한 데이터인 ownedTokenByAccountData를 받아 테이블 요소에 바인딩 합니다.

//TokenTable.tsx

import {
  TokenTableProps,
  TokensOwnedByAccountResponse,
} from "./interface";

const TokenTable = ({ ownedTokensByAccountData }: TokenTableProps) => {

  return (
    <div className="flex flex-col items-center justify-center">
      <div className="mt-10 text-2xl font-bold">Token List</div>
      {ownedTokenByAccountData && ownedTokenByAccountData.length > 0 ? (
          <table className="w-full max-w-7xl table-fixed border-collapse mt-5 mb-10 shadow-xl shadow-black ">
            <thead>
              <tr className="border-2 border-noditGreen bg-noditGreen text-white ">
                <th className="p-5">Number</th>
                <th className="p-5">Name</th>
                <th className="p-5">Symbol</th>
                <th className="p-5">Decimals</th>
                <th className="p-5">Balances</th>
              </tr>
            </thead>
            <tbody>
              {ownedTokenByAccountData.map(
                (item: TokensOwnedByAccountResponse, index: number) => (
                  <tr
                    key={item.contract.deployedTransactionHash}
                    className="border border-noditGreen hover:scale-105 duration-100 cursor-pointer "
                  >
                    <th className="font-bold p-5">
                      {ownedTokenByAccountData.page === 1
                        ? index + 1
                        : ownedTokenByAccountData.page * 20 + index - 19}
                    </th>
                    <th className="font-light p-5">{item.contract.name}</th>
                    <th className="font-light p-5">{item.contract.symbol}</th>
                    <th className="font-light p-5">{item.contract.decimals}</th>
                    <th className="font-light p-5">{item.balance}</th>
                  </tr>
                )
              )}
            </tbody>
          </table>
      ) : (
        <div>This Account doesn't have any Tokens</div>
      )}
    </div>
  );
};

export default TokenTable;

부모 컴포넌트에서는 API의 데이터를 호출하고 테이블 컴포넌트에 API 호출 결과 데이터를 전달합니다.

// TokenList.tsx

import React from "react";
import {
  useGetTokensOwnedByAccount,
} from "./useQueries";
import { useParams } from "react-router-dom";
import TokenTable from "./components/TokenTable";

const TokenList = (): React.ReactElement => {
  const { accountAddress } = useParams();
  if (!accountAddress) throw new Error("Check your account address");
  const {
    isError,
    data: getTokensOwnedByAccountData,
    isLoading,
  } = useGetTokensOwnedByAccount(accountAddress, currentPage);

  if (isLoading) return <div>Loading...</div>;

  if (isError) return <div>You have to connect node</div>;

  return (
    <div>
      <TokenTable
        ownedTokensByAccountData={getTokensOwnedByAccountData?.data}
      />
    </div>
  );
};

export default TokenList;

위와 같이 작성 후 서버를 실행하면 사진과 같이 데이터를 화면에서 확인할 수 있습니다.

스크린샷 2024-08-30 오후 6.55.39.png

Step 4. 예제 코드

위에서 구현한 내용을 포함한, 미리 만들어놓은 예제 프로젝트를 공유해 드립니다.

  • 소스 코드 다운로드 및 접근
$ git clone https://github.com/Lambda256/Nodit-EVM-Tutorials
$ cd Nodit-EVM-Tutorials
$ cd token_tutorial
  • 프로젝트 초기화 및 라이브러리 설치
$ npm install

사용자가 이용할 프로토콜과 네트워크 그리고 사용자의 Nodit API Key를 환경 변수로 설정합니다. Nodit API Key와 프로토콜, 네트워크의 경우 Nodit Console에서 확인할 수 있으며 기본 값으로 입력되어 있는 ethereum과 mainnet 외에도 사용자가 연결한 노드가 있다면 이용할 수 있습니다.

// .env

VITE_API_KEY=Your_Nodit_API_Key
VITE_PROTOCOL=ethereum
VITE_NETWORK=mainnet

환경 설정을 완료한 후 터미널에 아래의 명령어를 입력하여 Nodit Token Tutorial을 실행합니다.

$ npm run dev
스크린샷 2024-08-30 오후 6.56.52.png

Congratulation! 🎊

Nodit Token Tutorial을 모두 완료하였습니다! 다른 Nodit API를 이용하여 나만의 기능을 추가해 보세요!

더욱 다양한 기능을 가진 Nodit의 API가 궁금하신가요? 아래 링크를 눌러 확인해 보세요!

Nodit API Reference 바로가기