APIs used:
  • Get historical portfolio value over time  

Are you a successful investor, or a degen with a large crypto bag? Have you ever wondered how you can track the historic value of your crypto holdings for the last 30/60/90 days or longer, to see which tokens have gone up, what has gone down, and the rate of change of each tokens? You’ve come to the right place.

In this how-to guide, you’ll be learning how to get the historic portfolio value data of all the tokens of a wallet or smart contract address, for up to 200+ chains, using the Covalent API. This includes chains like Ethereum, Polygon, Avalanche, BSC, Arbitrum, Oasis, Optimism, and many more.

By the end of this guide, you’ll be able to build a historic holdings component that looks like this:

Let’s get started!

(Estimated time to follow along: 15mins)

Prerequisites

  • Basic familiarity with React.

  • Some HTML/CSS knowledge.

  • Fetching data using APIs.

Tutorial: Building a Historical Crypto Portfolio Chart

1

Initialize a React Project

npx create-react-app portfolio-chart
2

Install the Required Libraries

We’ll be using Recharts, a beautiful charting library for our purpose.

npm i recharts
3

Fetch the Data

The endpoint we’ll be using is Covalent’s Get historic portfolio value over time endpoint. Let’s define it within App.js, along with the chain name and a sample wallet address to use:

const chainName = 'eth-mainnet'
const walletAddress = '0x6564466f510c8311FDF935C5B2566201AAdFceA3' // a sample wallet address
const apiKey = process.env.REACT_APP_COVALENT_API_KEY
const historicPortfolioValueEndpoint = `https://api.covalenthq.com/v1/${chainName}/address/${walletAddress}/portfolio_v2/`
3.1

Be sure to create a .env file at your project root and store your API key within it, like so:

If you don’t have a Covalent API key, you can get it for free here.

REACT_APP_COVALENT_API_KEY='ckey_xx8c4xxxxxxxxxxxxx5008'
3.2

Within your App.js, fetch the data using an effect hook:

function App() {

  const [data, setData] = useState(null);
  const [keys, setKeys] = useState(null);

  useEffect(() => {
    fetch(historicPortfolioValueEndpoint, {method: 'GET', headers: {
      "Authorization": `Basic ${btoa(apiKey + ':')}`
    }})
			.then(res => res.json())
      .then(res => {
        const rawData = res.data.items
        console.log(rawData)
      })
  }, [])
3.3

Run npm start, and head over to your dev console.

You should be able to see this:

Congratulations, you’ve successfully retrieved the data!

4

Examining the Response

Before we pipe the data into our chart, let’s take a look at what the response looks like:

Essentially, we have an array of objects. Each object represents a particular token holding, with base-level attributes such as contract_ticker_symbol and its logo_url.


// Response for the Get historic portfolio value over time endpoint
[
  {
    "contract_decimals": 18,
    "contract_name": "Ether",
    "contract_ticker_symbol": "ETH",
    "contract_address": "0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee",
    "supports_erc": null,
    "logo_url": "<https://logos.covalenthq.com/tokens/1/0xeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee.png>",
    "holdings": [...]
  },
  {
    "contract_decimals": 18,
    "contract_name": "Dai Stablecoin",
    "contract_ticker_symbol": "DAI",
    "contract_address": "0x6b175474e89094c44da98b954eedeac495271d0f",
    "supports_erc": null,
    "logo_url": "<https://logos.covalenthq.com/tokens/1/0x6b175474e89094c44da98b954eedeac495271d0f.png>",
    "holdings": [...]
	},
	{
    "contract_decimals": 18,
    "contract_name": "Fantom Token",
    "contract_ticker_symbol": "FTM",
    "contract_address": "0x4e15361fd6b4bb609fa63c81a2be19d873717870",
    "supports_erc": null,
    "logo_url": "<https://logos.covalenthq.com/tokens/1/0x4e15361fd6b4bb609fa63c81a2be19d873717870.png>",
    "holdings": [...]
  }
...// more items below
]
4.1

The time series data is contained within the holdings attribute. Here is what the array looks like when we expand it:

This is a classic time-series array of OHLC quotes. Each holdings array contains 30 items by default, which can be changed with the days query parameter.

"holdings": [
		{
      "timestamp": "2023-04-13T00:00:00Z",
      "quote_rate": 1965.6401,
      "open": {
        "balance": "12980227189076782647",
        "quote": 25514.457
      },
      "high": {
        "balance": "12980227189076782647",
        "quote": 25514.457
      },
      "low": {
        "balance": "12980227189076782647",
        "quote": 25514.457
      },
      "close": {
        "balance": "12980227189076782647",
        "quote": 25514.457
      }
    },
    {
      "timestamp": "2023-04-12T00:00:00Z",
      "quote_rate": 1919.6385,
      "open": {
        "balance": "12980227189076782647",
        "quote": 24917.346
      },
      "high": {
        "balance": "12980227189076782647",
        "quote": 24917.346
      },
      "low": {
        "balance": "12980227189076782647",
        "quote": 24917.346
      },
      "close": {
        "balance": "12980227189076782647",
        "quote": 24917.346
      }
    }
...
]
5

Transform the Data

Before we feed this data into our chart, we need to see what data structure Rechart’s line chart expects.

Heading over to the Recharts documentation of a line chart, we see that it requires the data object to be an array of objects of single-layer nesting.


const data = [
  {
    name: 'Page A',
    uv: 4000,
    pv: 2400,
    amt: 2400,
  },
  {
    name: 'Page B',
    uv: 3000,
    pv: 1398,
    amt: 2210,
  }
...
5.1

The component rendered, <LineChart />, takes this data array as a prop.


return (
        <LineChart
          width={500}
          height={300}
          data={data}
          margin={{
            top: 5,
            right: 30,
            left: 20,
            bottom: 5,
          }}
        >
        ...
        </LineChart>
)
5.2

We also see that the <Line /> component takes the attribute name to render the Y-values.

<Line type="monotone" dataKey="pv" stroke="#8884d8" activeDot={{ r: 8 }} />
5.3

This means that we’ll need to transform our data response to something of this shape:

Each object in the array has a timestamp attribute that corresponds to an individual day, and all the token balances for that day are condensed into that object.


[
	{
		timestamp: "2023-04-13T00:00:00Z",
		ETH: 25514.457,
		USDC: 1330,
		UNI: 90,
		...
	},
	{
		timestamp: "2023-04-14T00:00:00Z",
		ETH: 25333.90,
		USDC: 1220,
		UNI: 10,
		...
	},

]
5.4

To do that, let’s write the following function that takes our raw Covalent API response and returns it in our desired format:

const transformForRecharts = (rawData) => {

  const transformedData = rawData.reduce((acc, curr) => {
    const singleTokenTimeSeries =  curr.holdings.map(holdingsItem => {
      return {
        timestamp: holdingsItem.timestamp,
        [curr.contract_ticker_symbol]: holdingsItem.close.quote
      }
    }) 
    const newArr = singleTokenTimeSeries.map((item, i) => Object.assign(item, acc[i]))
    return newArr
  }, [])

  return transformedData

}
5.5

Applying the function to our response in the effect hook:

useEffect(() => {
    fetch(historicPortfolioValueEndpoint, {method: 'GET', headers: {
      "Authorization": `Basic ${btoa(apiKey + ':')}`
    }})
			.then(res => res.json())
      .then(res => {
        const rawData = res.data.items
        const transformedData = transformForRecharts(rawData)
        console.log(transformedData)
				setData(transformedData)
      })
  }, [])
5.6

and console logging, we get this:

Perfect.

Time to pipe it into the chart!

6

Render the Chart

Create a LineChart component as per the instructions in the Recharts documentation.


return (
      <LineChart
        width={800}
        height={500}
        data={data}
        margin={{
          top: 5,
          right: 30,
          left: 20,
          bottom: 5,
        }}
      >
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="timestamp" />
        <YAxis />
        <Tooltip />
        <Legend />
        {keys.map((item, i) => {
          return (
            <Line dataKey={item} type="monotone" stroke={colors[i]}/>
          )
        })}
      </LineChart>
)
6.1

Your entire App.js component will now look like this:

JavaScript
import React, { useState, useEffect } from 'react';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts';

const apiKey = process.env.REACT_APP_COVALENT_API_KEY
const chainName = 'eth-mainnet'
const walletAddress = '0x6564466f510c8311FDF935C5B2566201AAdFceA3'
const historicPortfolioValueEndpoint = `https://api.covalenthq.com/v1/${chainName}/address/${walletAddress}/portfolio_v2/`

const colors = ["#F44336", "#673AB7", "#03A9F4", "#4CAF50", "#FFEB3B", "#FF5722", "#607D8B", "#E91E63", "#3F51B5", "#00BCD4", "#8BC34A", "#FFC107", "#795548", "#9C27B0", "#2196F3", "#009688", "#CDDC39", "#FF9800", "#9E9E9E", "#EF9A9A", "#B39DDB", "#81D4FA", "#A5D6A7", "#FFF59D", "#FFAB91", "#B0BEC5", "#F48FB1", "#9FA8DA", "#80DEEA", "#C5E1A5", "#FFE082", "#BCAAA4", "#CE93D8", "#90CAF9", "#80CBC4", "#E6EE9C", "#FFCC80", "#EEEEEE", "#B71C1C", "#311B92", "#01579B", "#1B5E20", "#F57F17", "#BF360C", "#263238", "#880E4F", "#1A237E", "#006064", "#33691E", "#FF6F00", "#3E2723", "#4A148C", "#0D47A1", "#004D40", "#827717", "#E65100", "#212121"]

function App() {

  const [data, setData] = useState(null);
  const [keys, setKeys] = useState(null);

  useEffect(() => {
    fetch(historicPortfolioValueEndpoint, {method: 'GET', headers: {
      "Authorization": `Basic ${btoa(apiKey + ':')}`
    }})
      .then(res => res.json())
      .then(res => {
        const rawData = res.data.items
        const transformedData = transformForRecharts(rawData)
        const dataKeys = rawData.map(item => item.contract_ticker_symbol)
        setKeys(dataKeys)
        setData(transformedData)
      })
  }, [])

  if (!data) {
    return <div>Loading...</div>;
  }

  return (
      <LineChart
        width={800}
        height={500}
        data={data}
        margin={{
          top: 5,
          right: 30,
          left: 20,
          bottom: 5,
        }}
      >
        <CartesianGrid strokeDasharray="3 3" />
        <XAxis dataKey="timestamp" />
        <YAxis />
        <Tooltip />
        <Legend />
        {keys.map((item, i) => {
          return (
            <Line dataKey={item} type="monotone" stroke={colors[i]}/>
          )
        })}
      </LineChart>
  )
};

export default App;

const transformForRecharts = (rawData) => {

  const transformedData = rawData.reduce((acc, curr) => {
    const singleTokenTimeSeries =  curr.holdings.map(holdingsItem => {

    // Formatting the date string just a little...
    const dateStr = holdingsItem.timestamp.slice(0,10)
    const date = new Date(dateStr)
    const options = {
      day: "numeric",
      month: "short"
    };
    const formattedDate = date.toLocaleDateString("en-US", options);
      return {
        timestamp: formattedDate,
        [curr.contract_ticker_symbol]: holdingsItem.close.quote
      }
    }) 
    const newArr = singleTokenTimeSeries.map((item, i) => Object.assign(item, acc[i]))
    return newArr
  }, [])

  return transformedData
}
6.2

Running npm start, you’ll be able to see the following:

All in a Day’s Work

Congratulations. You’ve successfully built a historical portfolio chart that can be used for all kinds of use cases.

While the immediate use case is to be able to track all your token holdings, you can also use this endpoint to:

  • Get the historical holdings value of a liquidity pool.

  • Getting historical holdings of a DAO treasury.

Basically, if you need to check the holdings of any on-chain address or smart contract, this endpoint will do the trick. The best part? Simply replace the chainName with any of the other supported values to get an address’ holdings in other chains.

And there you have it, 200+ chains cross-chain historical token portfolio chart at your fingertips!