Fetching NFTs, Stacks and Hiro API

Explanations

Fetch all NFTs owned by user address

Not we get to the NFT integration step. This is where all the fun starts. We want our app to show a list of all NFTs owned by the user and render them to the MainMenu component. For this, we will need to ask query the Stacks API. How can a developer query an online API? Obviously, making URL requests.

Stacks API URL

The URL we have to request for fetching the stacks API looks like this:

https://stacks-node-api.mainnet.stacks.co/extended/v1/tokens/nft/holdings?principal=SP1SCEXE6PMGPAC6B4N5P2MDKX8V4GF9QDE1FNNGJ&&asset_identifiers=SP1SCEXE6PMGPAC6B4N5P2MDKX8V4GF9QDE1FNNGJ.workshop-nfts-integration::duck

https://stacks-node-api.testnet.stacks.co/ is the official Stacks Testnet node and what is after it is the specific API to get the NFTs owned by a given Stacks Address.

JSON Response Structure

It contains the principal address of the user, the wallet address, the smart contract we want to call and the function name. Let’s try it by searching on the browser url section and hitting enter. The response from the API looks something like this:

{
  "limit": 50,
  "offset": 0,
  "total": 4,
  "results": [
    {
      "asset_identifier": "ST28AA1PJD8615MPKKKSSX311EWY000G77SFEXXHX.my-nft::character",
      "value": {
        "hex": "0x0100000000000000000000000000000002",
        "repr": "u2"
      },
      "block_height": 81902,
      "tx_id": "0xb7a5d44a610d1e1ead9d8e5d34909600fe566550a9c7c63ce8c8bd585f121c89"
    },
    {
      "asset_identifier": "ST28AA1PJD8615MPKKKSSX311EWY000G77SFEXXHX.my-nft::character",
      "value": {
        "hex": "0x0100000000000000000000000000000001",
        "repr": "u1"
      },
      "block_height": 81902,
      "tx_id": "0x158d69b6877120d8ea83ee94bb7a90bf9074a8c39854ce55eee0dff5599d9c45"
    },
    {
      "asset_identifier": "ST28AA1PJD8615MPKKKSSX311EWY000G77SFEXXHX.my-nft::character",
      "value": {
        "hex": "0x0100000000000000000000000000000003",
        "repr": "u3"
      },
      "block_height": 81902,
      "tx_id": "0x62aeed938b57da7adc473d8017792cc13cee714e4d93fdc829c57b5aaff5b980"
    },
    {
      "asset_identifier": "ST28AA1PJD8615MPKKKSSX311EWY000G77SFEXXHX.my-nft::character",
      "value": {
        "hex": "0x0100000000000000000000000000000004",
        "repr": "u4"
      },
      "block_height": 81950,
      "tx_id": "0x19300a8d2663b02e0012be879597372cf4dece9c09128cf2069ea46a98bc07ba"
    }
  ]
}

Code inserted

Function to get ids of NFTs from JSON

Let’s explain what have you seen above. It is observable that we have got a JSON structure as a response. This structure contains limit, offset, total and results. The limit is the maximum number of results we want to get as a response. The default value for the Stacks API is 50. The offset represents the value from which we want to fetch the NFTs. This means if, for example, we own 51 NFTs, we will make two requests limited to 50 results. The first will have the offset 0, the second will have the offset 50. The total represents the total number of NFTs the user has for the requested address. Least but not last, we have the results. This array will contain either all the NFTs owned, if the total is lower than the limit, or a number of NFTs equal to the limit if the total is greater than the limit. It is easy to find out that we will have to process the result in order to get a list of ids owned by the user. For this, we will need a function that will look like this:

const getIDsNFTsOwned = (jsonNFTHoldings) => {
  let ids = [];
  if (jsonNFTHoldings.results) {
    jsonNFTHoldings.results.map((x) => {
      const id = x.value.repr.substring(1).toString();
      if (id != '') ids.push(id);
    });
  }
  return ids;
};

Function to fetch all the JSON responses from the API and get all the ids of the owned NFTs

You can see above that the function getIDsNFTsOwned only has one parameter, NFTPartsJson. It refers to the JSON response we get as a result after sending the url request to the API.

Having these explained, let’s build another function that, builds the url, sends the request to the API, waits to receive the response and interprets it. The function should look like this:

const getNFTsOwned = async (accountAddress) => {
  const limit = 50;
  let offsetNFT = 0;
  const asset_identifiers = 'ST28AA1PJD8615MPKKKSSX311EWY000G77SFEXXHX.my-nft::character';
  let urlHoldingNFT = `https://stacks-node-api.testnet.stacks.co/extended/v1/tokens/nft/holdings?principal=${accountAddress}&&asset_identifiers=${asset_identifiers}`;
  let jsonNFT = await fetch(urlHoldingNFT).then((res) => {
    return res.json();
  });

  let listOfNFTs = [];
  listOfNFTs = getIDsNFTsOwned(jsonNFT);
  const total = jsonNFT.total;
  offsetNFT += limit;
  while (offsetNFT < total) {
    const offsetNFTurl = `&&offset=${offsetNFT}`;
    const currentUrlHoldingNFT = urlHoldingNFT + offsetNFTurl;
    jsonNFT = await fetch(currentUrlHoldingNFT).then((res) => {
      return res.json();
    });
    listOfNFTs = listOfNFTs.concat(getIDsNFTsOwned(jsonNFT));
    offsetNFT += limit;
  }
  console.log('NFTs owned: ', listOfNFTs.toString());
  return listOfNFTs;
};

Explanations for the `getNFTsOwned` function

Let’s split it to make it easier to understand.

let offsetNftParts = 0;
const asset_identifiers = 'ST28AA1PJD8615MPKKKSSX311EWY000G77SFEXXHX.my-nft::character';
let urlHoldingNFT = `https://stacks-node-api.testnet.stacks.co/extended/v1/tokens/nft/holdings?principal=${accountAddress}&&asset_identifiers=${asset_identifiers}`;

The code snippet above shows the way we build the url for the API request. We initialize the offset to 0, the asset identifiers to the value we need containing our smart contract and function name. Then, we build the url. The accountAddress is our function parameter. After creating the url string, we will send the request to the API using fetch() method and wait for the response, which will be stored using the nftParts variable.

let jsonNFT = await fetch(currentUrlHoldingNFT)
  .then((res) => {
    return res.json();
  });

Having stored the request’s result, we should operate on it. We will obtain a list of ids from the response using the previously explained getIDsNFTsOwned() function and storing it into the listOfNfts variable. You can see the function’s parameter is await nftParts, which does nothing but wait for the API’s response. A constant to memorize the total number of NFTs owned by the user is needed, too. Let’s see it in the console!

let listOfNFTs = [];
listOfNFTs = getIDsNFTsOwned(jsonNFT);
const total = jsonNFT.total;

Next, we will make sure we have got all the NFTs the user owns and for this we will need to increment the offset. Assuring that we have fetched all the NFTs owned by the user will be made using a while repetitive structure. As you see below, the execution thread will execute the while structure only if the previously incremented offset is lower than the total number of results. This way we will make sure on the one hand if we have already covered the results, the while structure will not be executed at all. On the other hand, if the total is greater than the offset, we will continue to search for NFTs as many times as needed and every time increment the offset by the limit’s value. As you have seen above, the limit’s value equals to 50 by default, so we initialize it. In order to fetch results using an offset different to 0, we have to concatenate &&offset=value to our request url.

offsetNFT += limit;

while (offsetNFT < total) {
  const offsetNFTurl = `&&offset=${offsetNFT}`;
  const currentUrlHoldingNFT = urlHoldingNFT + offsetNFTurl;
  jsonNFT = await fetch(currentUrlHoldingNFT).then((res) => {
    return res.json();
  });
  listOfNFTs = listOfNFTs.concat(getIDsNFTsOwned(jsonNFT));
  offsetNFT += limit;
}

The code snippet above shows the fact that for every while step, we concatenate the new obtained list of ids to the previous one. This way we make sure we have stored all the results.

Let’s view the results we have obtained into the console and return them!

console.log('NFTs owned: ', listOfNfts.toString());
return listOfNFTs;

Recap how the final code should look in MainMenu file

const getIDsNFTsOwned = (jsonNFTHoldings) => {
  let ids = [];
  if (jsonNFTHoldings.results) {
    jsonNFTHoldings.results.map((x) => {
      const id = x.value.repr.substring(1).toString();
      if (id != '') ids.push(id);
    });
  }
  return ids;
};

const getNFTsOwned = async (accountAddress) => {
  const limit = 50;
  let offsetNFT = 0;
  const asset_identifiers = 'ST28AA1PJD8615MPKKKSSX311EWY000G77SFEXXHX.my-nft::character';
  let urlHoldingNFT = `https://stacks-node-api.testnet.stacks.co/extended/v1/tokens/nft/holdings?principal=${accountAddress}&&asset_identifiers=${asset_identifiers}`;
  let jsonNFT = await fetch(urlHoldingNFT).then((res) => {
    return res.json();
  });

  let listOfNFTs = [];
  listOfNFTs = getIDsNFTsOwned(jsonNFT);
  const total = jsonNFT.total;
  offsetNFT += limit;
  while (offsetNFT < total) {
    const offsetNFTurl = `&&offset=${offsetNFT}`;
    const currentUrlHoldingNFT = urlHoldingNFT + offsetNFTurl;
    jsonNFT = await fetch(currentUrlHoldingNFT).then((res) => {
      return res.json();
    });
    listOfNFTs = listOfNFTs.concat(getIDsNFTsOwned(jsonNFT));
    offsetNFT += limit;
  }
  console.log('NFTs owned: ', listOfNFTs.toString());
  return listOfNFTs;
};

This is the branch with the changes done:

branch dapp/2-fetch-nfts-owned

You can check the exact changes in this commit referring to them

commit 5182629178e261c4a09a637d13509c7a7cb52700

Last updated