Building an NFT Collection Holder Snapshot App with Near Social

Joshua OwenJoshua Owen
Mar 15, 2023|5 min read

Introduction

As the popularity of NFTs continues to grow, it becomes increasingly valuable to have a tool that can quickly fetch, display, and export NFT data from any collection on the NEAR blockchain. In this blog post, we will guide you through the process of building an NFT Collection Holder Snapshot App using Near Social.

See the app live here: https://alpha.near.org/#/9c461db4ac11b66ed1167ff969042ac278eaf2d571712585424be00171a63884/widget/NFT-Collection-Holder-Snapshot

Overview

The NFT Collection Holder Snapshot App allows users to:

  1. Fetch NFT data from any collection on the NEAR blockchain.
  2. Display the information in a table format with pagination.
  3. Export the data as a CSV file.

We will build this app using Near Social and the Indexer.xyz API.

Prerequisites

To build this app, you will need:

  1. Basic knowledge of JavaScript.
  2. Familiarity with the Near Social platform.

Steps to Build the App

  1. Set up the project and state management.
  2. Fetch NFT data using the Indexer.xyz API.
  3. Prepare holder data.
  4. Display the data in a table format with pagination.
  5. Export the data as a CSV file.
  6. Finalize the UI.

Step 1: Setting up the project and state management

To set up our project, we will use Near Social to create a new app. Then, we'll define our initial state, which includes variables such as slug, nftData, holdersData, currentPage, and rowsPerPage.

initState({
  slug: "asac.near",
  nftData: [],
  holdersData: [],
  currentPage: 0,
  rowsPerPage: 10,
  downloadLink,
});

Step 2: Fetching NFT data using Indexer.xyz API

In this step, we'll create a function called fetchData that will fetch NFT data from the given collection using the Indexer.xyz API. We'll make a POST request to the API, passing in the collection contract key (slug) as a query parameter.

const fetchData = () => {
  State.update({ nftData: [] });
  console.log("fetchData()");
  const { slug } = state;
  console.log(slug);

  let data = fetch("<https://byz-multi-chain-01.hasura.app/v1/graphql>", {
    method: "POST",
    headers: {
      "x-api-key": "ChRbeKE.c94220449dbb45973a67a614b1f590be",
      "Content-Type": "application/json",
      "Hasura-Client-Name": "near-social",
    },
    body: JSON.stringify({
      query: `
      query MyQuery {
  near {
    collection(where: {slug: {_eq: "${state.slug}"}}) {
      slug
      nft_metas (order_by: {token_id: asc}) {
        token_id
        nft_state {
          owner
          staked_owner
        }
      }
    }
  }
}
`,
    }),
  });
  if (data) {
    const nftData = data.body.data.near.collection[0].nft_metas;
    console.log("here ---->>>>", nftData);
    State.update({ nftData });
    prepareHoldersData();
  }
};

Step 3: Preparing holder data

After fetching the NFT data, we need to prepare the holder data for display. We'll create a function called prepareHoldersData that will iterate through the fetched NFT data and create an object with information about each holder, including the number of tokens they own, the number of staked tokens, and the specific tokens they hold and stake.

const prepareHoldersData = () => {
  const holders = {};

  state.nftData.forEach((token) => {
    const owner = token.nft_state.owner;
    const staked_owner = token.nft_state.staked_owner;

    if (owner) {
      if (!holders[owner]) {
        holders[owner] = { count: 0, staked: 0, tokens: [], stakedTokens: [] };
      }
      if (!staked_owner || owner !== staked_owner) {
        holders[owner].count += 1;
        holders[owner].tokens.push(token.token_id);
      }
    }

    if (staked_owner && owner !== staked_owner) {
      if (!holders[staked_owner]) {
        holders[staked_owner] = {
          count: 0,
          staked: 0,
          tokens: [],
          stakedTokens: [],
        };
      }
      holders[staked_owner].staked += 1;
      holders[staked_owner].stakedTokens.push(token.token_id);
    }
  });

  const holdersData = Object.entries(holders)
    .map(([owner, data]) => ({ owner, ...data }))
    .sort((a, b) => b.count + b.staked - (a.count + a.staked));

  State.update({ holdersData });
};

Step 4: Displaying the data in a table format with pagination

With the holder data prepared, we can now display it in a table format. We'll create a table with columns for the wallet, number of tokens, number of staked tokens, tokens held, and tokens staked. Additionally, we'll implement pagination to navigate through the data easily.

<table
  style={{
    borderCollapse: "collapse",
    width: "100%",
    fontFamily: "Arial, sans-serif",
    fontSize: "14px",
  }}
>
  <thead>
    <tr>
      <th>Wallet</th>
      <th>Number of Tokens</th>
      <th>Number of Staked Tokens</th>
      <th>Tokens Held</th>
      <th>Tokens Staked</th>
    </tr>
  </thead>
  <tbody>
    {state.holdersData
      .slice(
        state.currentPage * state.rowsPerPage,
        (state.currentPage + 1) * state.rowsPerPage
      )
      .map((holder) => (
        <tr>
          <td style={{ padding: "10px" }}>
            <a
              href={`https://www.tradeport.xyz/near/profile/${holder.owner}?tab=collected`}
              target="_blank"
              rel="noopener"
            >
              {holder.owner.length > 30
                ? holder.owner.slice(0, 30) + "..."
                : holder.owner}
            </a>
          </td>
          <td style={{ padding: "10px" }}>{holder.count}</td>
          <td style={{ padding: "10px" }}>{holder.staked}</td>
          <td style={{ padding: "10px" }}>
            <div
              style={{
                maxHeight: "100px",
                overflowY: "scroll",
                width: "300px",
                border: "1px solid #ccc",
                padding: "5px",
              }}
            >
              {holder.tokens
                .filter(
                  (tokenId) => !holder.stakedTokens.includes(tokenId)
                )
                .map((tokenId, index, filteredTokens) => (
                  <div key={tokenId} style={{ display: "inline" }}>
                    <a
                      href={`https://www.tradeport.xyz/near/collection/${state.slug}/token/${tokenId}} target="_blank" rel="noopener" > {tokenId} </a> {index < filteredTokens.length - 1 && ", "} </div> ))} </div> </td> <td style={{ padding: "10px" }}> <div style={{ maxHeight: "100px", overflowY: "scroll", width: "300px", border: "1px solid #ccc", padding: "5px", }} > {holder.stakedTokens.map((tokenId, index) => ( <div key={tokenId} style={{ display: "inline" }}> <a href={<https://www.tradeport.xyz/near/collection/${state.slug}/token/${tokenId}`>}
target="_blank"
rel="noopener"
>
{tokenId}
</a>
{index < holder.stakedTokens.length - 1 && ", "}
</div>
))}
</div>
</td>
</tr>
))}

  </tbody>
</table>

Step 5: Exporting the data as a CSV file

To export the holder data as a CSV file, we'll create a function called exportToCsv that will convert the data to a CSV format and then create a download link.

const exportToCsv = () => {
  const header = [
    "Wallet",
    "Number of Tokens",
    "Number of Staked Tokens",
    "Tokens Held",
    "Tokens Staked",
  ];
  const rows = state.holdersData.map((holder) => [
    holder.owner,
    holder.count,
    holder.staked,
    holder.tokens.filter((tokenId) => !holder.stakedTokens.includes(tokenId)),
    holder.stakedTokens,
  ]);

  const csvContent =
    "data:text/csv;charset=utf-8," +
    [header, ...rows].map((e) => e.join(",")).join("\\n");

  State.update({ downloadLink: csvContent });
};

Step 6: Finalizing the UI

With all the functionality in place, we'll now finalize the UI by adding input fields to enter the collection slug, buttons to fetch the data and export it as a CSV file, and pagination controls.

<div>
  <input
    type="text"
    value={state.slug}
    onChange={(e) => State.update({ slug: e.target.value })}
    placeholder="Enter collection slug"
  />
  <button onClick={fetchData}>Fetch Data</button>
  <br />
  <br />
  <button onClick={exportToCsv}>
    <a
      download={`${state.slug}-holders.csv`}
      href={state.downloadLink}
      onClick={(e) => {
        if (!state.downloadLink) e.preventDefault();
      }}
    >
      Export to CSV
    </a>
  </button>
  <br />
  <br />
  <nav>
    <button
      onClick={() =>
        State.update({ currentPage: Math.max(0, state.currentPage - 1) })
      }
    >
      &lt; Previous
    </button>
    <span>
      {" "}
      Page {state.currentPage + 1} of{" "}
      {Math.ceil(state.holdersData.length / state.rowsPerPage)}{" "}
    </span>
    <button
      onClick={() =>
        State.update({
          currentPage: Math.min(
            Math.ceil(state.holdersData.length / state.rowsPerPage) - 1,
            state.currentPage + 1
          ),
        })
      }
    >
      Next &gt;
    </button>
  </nav>
  <br />
</div>

Conclusion

In this blog post, we have laid out the framework for constructing an NFT Collection Holder Snapshot App using Near Social. The app fetches NFT data from a specific collection on the NEAR blockchain, displays it in a table format with pagination, and enables its users to export the data as a CSV file. With this app, users are empowered to easily gain an overview of NFT holders and their holdings in any collection on the NEAR blockchain.

As NFTs continue their meteoric rise in popularity, tools such as the NFT Collection Holder Snapshot App become increasingly invaluable for collectors, traders, and creators alike. Furthermore, this app can be further enhanced with additional features, such as filtering or sorting the data, integrating with other APIs, or adding more visualization options.

Happy coding!