APIs used:
  • Get token approvals for address  

Have you ever felt uneasy about the different dApps you approved to spend ERC20 tokens from your account? With the many hacks and scams in the space, it is important to be mindful of what you approved. The consequence of not doing so is the possibility of losing all your tokens to a malicious contract.

Covalent's Token Approvals API Endpoint

However, there is no easy way to check which contracts you’ve approved to spend your tokens. Luckily, with Covalent’s new approvals endpoint, you can easily check all the approvals given for a wallet address. The endpoint even comes with a special field: risk_factor, that alerts you to the level of risk inherent in the particular approval, so that based on the assessment, you can decide on whether to revoke the approval or not.

With just one API call, you can easily build the following ‘approvals checker’ feature into your app:

In this tutorial, we’ll be building the component above.

Prerequisites

  • Basic familiarity with React.

  • Some HTML/CSS knowledge.

  • Fetching data using APIs.

Tutorial: Building a Web3 Token Approvals Checker

Ready? Let’s get started!

(Estimated time to follow along: 20mins)

1

Figuring out the data you need

Approvals are done on a per-token basis. That means, for each individual contract you’ve approved, you’re approving it for a particular token in your wallet. In the first row, we are displaying details of an individual token in your wallet: i.e. the token name (Dai Stablecoin) and token balance (17237.05), etc. The subsequent rows unfolding beneath it display the contracts we’ve approved to spend on behalf of the token. Each row contains the following data points:

  • Contract name - e.g. “Uniswap V2: Router 2”

  • Contract address - e.g. “0x725…488d”

  • Allowance amount - e.g. “Unlimited”

  • Value at risk - e.g. “$17237.05”

  • Date approved - “2021-06-27”

  • Risk factor - in this case, we render either a red warning triangle for high-risk, a stop sign for mid-risk, or a tick for low-risk.

All of these data can be found using Covalent’s approvals endpoint.

2

Examining the data

Here’s what the request path looks like for the approvals endpoint:

https://api.covalenthq.com/v1/1/approvals/{walletAddress}/?key={apiKey}

ℹ️ If you want to try it for yourself, be sure to replace the walletAddress and apiKey variables, and pop the endpoint into the browser. If you don’t have a Covalent API key, you can get it for free here.

This is what the response looks like:

JavaScript
{
  "data": {
    "address": "0x0b17cf48420400e1d71f8231d4a8e43b3566bb5b",
    "updated_at": "2023-05-17T02:19:58.025009067Z",
    "quote_currency": "USD",
    "chain_id": 1,
    "chain_name": "eth-mainnet",
    "items":
      {
        "token_address": "0x6b175474e89094c44da98b954eedeac495271d0f",
        "token_address_label": "Dai Stablecoin",
        "ticker_symbol": "DAI",
        "contract_decimals": 18,
        "logo_url": "<https://logos.covalenthq.com/tokens/1/0x6b175474e89094c44da98b954eedeac495271d0f.png>",
        "quote_rate": 1,
        "balance": "17237053466447503632593",
        "balance_quote": 17237.053466447505,
        "pretty_balance_quote": "$17,237.05",
        "value_at_risk": "17237053466447503632593",
        "value_at_risk_quote": 17237.053466447505,
        "pretty_value_at_risk_quote": "$17,237.05",
        "spenders": [...] // Collapsed
      },
      {
        "token_address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
        "token_address_label": "Wrapped Ether",
        "ticker_symbol": "WETH",
        "contract_decimals": 18,
        "logo_url": "<https://logos.covalenthq.com/tokens/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2.png>",
        "quote_rate": 1829.37,
        "balance": "4328835830984884227",
        "balance_quote": 7919.042404128818,
        "pretty_balance_quote": "$7,919.04",
        "value_at_risk": "1400000000000000000",
        "value_at_risk_quote": 2561.118,
        "pretty_value_at_risk_quote": "$2,561.12",
        "spenders": [...] // Collapsed
      }
    ...
    ]
  },
  "error": false,
  "error_message": null,
  "error_code": null
}

As you can see, each item in the array corresponds to a token the user holds. The base attributes of the item are data pertaining to the individual token, such as token name (token_address_label), token ticker (ticker_symbol), token balance (balance), etc. All these are useful for rendering the first row in our example above.

Information pertaining to approved contracts is located in the spenders attribute (which I’ve collapsed in the example above). Here is what an uncollapsed spenders array looks like:

JavaScript
"spenders": [
         {
          "block_height": 12717067,
          "tx_offset": 243,
          "log_offset": 201,
          "block_signed_at": "2021-06-27T16:11:37Z",
          "tx_hash": "0x59dc0a479c42c77d9eae88fafdcc34811ab71306c26ae582fa71c71263fb9723",
          "spender_address": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
          "spender_address_label": "Uniswap V2: Router 2",
          "allowance": "UNLIMITED",
          "allowance_quote": null,
          "pretty_allowance_quote": null,
          "value_at_risk": "17237053466447503632593",
          "value_at_risk_quote": 17237.053466447505,
          "pretty_value_at_risk_quote": "$17,237.05",
          "risk_factor": "HIGH RISK, REVOKE NOW"
        },
        {
          "block_height": 13082532,
          "tx_offset": 440,
          "log_offset": 359,
          "block_signed_at": "2021-08-23T16:09:03Z",
          "tx_hash": "0x9ecf2ad560a778673f78fbb2cf12ded7b4b1c577cba7453b75432e733bfe199a",
          "spender_address": "0xea9265a4ffa137523e83d68d4160325a936ec8b9",
          "spender_address_label": "Early Bird",
          "allowance": "200000000000000000000",
          "allowance_quote": 200.007816,
          "pretty_allowance_quote": "$200.01",
          "value_at_risk": "200000000000000000000",
          "value_at_risk_quote": 200,
          "pretty_value_at_risk_quote": "$200.00",
          "risk_factor": "CONSIDER REVOKING"
        },
        {
          "block_height": 15497398,
          "tx_offset": 127,
          "log_offset": 199,
          "block_signed_at": "2022-09-08T15:34:05Z",
          "tx_hash": "0xd80657d7892d10528e4fedc571bbfb498e9012493560f3969c549a7a21032f6b",
          "spender_address": "0x7d655c57f71464b6f83811c55d84009cd9f5221c",
          "spender_address_label": "Gitcoin: Bulk Checkout",
          "allowance": "10500000000000000000",
          "allowance_quote": 10.500410339999998,
          "pretty_allowance_quote": "$10.50",
          "value_at_risk": "10500000000000000000",
          "value_at_risk_quote": 10.5,
          "pretty_value_at_risk_quote": "$10.50",
          "risk_factor": "LOW RISK"
        },
        {
          "block_height": 16182488,
          "tx_offset": 141,
          "log_offset": 646,
          "block_signed_at": "2022-12-14T10:58:47Z",
          "tx_hash": "0x9f43c08ab1dccd4b18f635d563e05e06b3a66e3b751ef0b055f64c3b1e2e6b69",
          "spender_address": "0x68b3465833fb72a70ecdf485e0e4c7bd8665fc45",
          "spender_address_label": null,
          "allowance": "UNLIMITED",
          "allowance_quote": null,
          "pretty_allowance_quote": null,
          "value_at_risk": "17237053466447503632593",
          "value_at_risk_quote": 17237.053466447505,
          "pretty_value_at_risk_quote": "$17,237.05",
          "risk_factor": "HIGH RISK, REVOKE NOW"
        }
	]

As you can see, each array item contains the data we’re interested in:

  • Contract name - spender_address_label

  • Contract address - spender_address

  • Allowance amount - allowance or pretty_allowance_quote

  • Value at risk - value_at_risk* or pretty_value_at_risk_quote

  • Date approved - block_signed_at

  • Risk factor - risk_factor**

(To understand how the fields value_at_risk and risk_factor are calculated, see appendix.)

We have all the data that we need. Let’s build!

3

Clone this starter kit & initialize the project

Run the following commands:

git clone https://github.com/xiaogit00/building-wallets.git

cd building-wallets

npm i

git checkout part5-approvals

npm start

Head over to http://localhost:3000 in your browser. You should see the following:

This is just a template to get you started. If you go to App.js, you’ll see that <TokenAllowance /> components are rendered based on the number of data items returned. Each <TokenAllowance /> component displays the <Token /> in question, along with all the <Contracts /> that have been approved to spend the token.

Everything is styled with App.css.

In the following steps, we’ll fetch the data, clean it, and supply it to our <TokenAllowance /> component.

4

Fetching the approvals data

As usual, fetch it in App.js via an effect hook like so, defining your endpoint and apiKey:

function App() {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const walletAddress = '0x0b17cf48420400e1D71F8231d4a8e43B3566BB5B'
  const approvalsEndpoint = `https://api.covalenthq.com/v1/1/approvals/${walletAddress}/`
  const apiKey = process.env.REACT_APP_APIKEY

  useEffect(() => {
    setLoading(true)
    fetch(approvalsEndpoint, {method: 'GET', headers: {
      "Authorization": `Basic ${btoa(apiKey + ':')}`
    }})
      .then(res => res.json())
      .then(res => {
        console.log(res.data.items)
        setData(res.data.items)
        setLoading(false)
      })
  }, [approvalsEndpoint, apiKey])

Running npm start and opening up the dev console, you should be able to see the following:

Congratulations, you’ve successfully fetched the data!

5

Rendering the data

In App.js, we’ve already supplied each item in the data array as a prop to the <TokenAllowance /> component.

This is the <TokenAllowance /> component:

const TokenAllowance = ( {tokenItem} ) => {
    return (
        <>
            <div className='tokenContainer'>
            <Token tokenItem={tokenItem}/>
            <div className='headers'>
                <div className='width-custom'>Risk Factor</div>
                <div>Allowance</div>
                <div>Value at Risk</div>
                <div>Date approved</div>
            </div>
            <Contracts spenders={tokenItem.spenders} />
            </div>
        </>
    )
}

export default TokenAllowance 

The <TokenAllowance /> component renders a <Token /> component, which takes the same tokenItem as a prop, and a <Contracts /> component, which takes the array in the spenders attribute as a prop (this is to render all the contracts within the spenders array).

The <Token /> component is as follows:

// <Token /> component
const Token = ( {tokenItem} ) => {
  
    const percentageAtRisk = tokenItem.balance_quote === 0 || tokenItem.value_at_risk_quote === 0 
                            ? '0' 
                            : (tokenItem.value_at_risk_quote*100/tokenItem.balance_quote).toFixed(2)
    return (
        <div className='tokenRow'>

					{/* Render Token Logo */}
            <div className='left'>
              <div className='logoContainer'>
                <img className='tokenLogo' src={tokenItem.logo_url} alt='tokenlogo'/>
              </div>

					{/* Render Token Name & Balance */}
              <div className='left-info-container'>
                <div className='tokenName'>{tokenItem.token_address_label}</div>
                <div className='tokenBalance'>{(Number(tokenItem.balance)/(10**tokenItem.contract_decimals)).toFixed(2)} {tokenItem.ticker_symbol}</div>
              </div>
            </div>

					{/* Render value & percentage at risk */}
            <div className='right'>
              <div className='tokenValue'>{tokenItem.pretty_balance_quote}</div>
              <div className='percentageChange'>{percentageAtRisk}% at risk</div>
            </div>
          </div>
    )
}

export default Token

Nothing fancy, really. All we’re doing is taking the base attributes of our response, cleaning them, and rendering them. The code above renders the following:

Then, everything beneath is rendered in the <Contracts /> component:

JavaScript
const Contracts = ( {spenders} ) => {
    
    const truncateRegex = /^(0x[a-zA-Z0-9]{4})[a-zA-Z0-9]+([a-zA-Z0-9]{4})$/

    const truncateEthAddress = (address) => {
        const match = address.match(truncateRegex);
        if (!match) return address;
        return `${match[1]}…${match[2]}`;
      };

    return (
        <>
        {spenders.map(item => {
            let riskImage 
            let riskTitle

            switch(item.risk_factor) {
                case 'HIGH RISK, REVOKE NOW':
                    riskImage = '<https://res.cloudinary.com/dl4murstw/image/upload/v1683203742/warning_1_dz7rpq.png>'
                    riskTitle = 'HIGH RISK, REVOKE NOW'
                    break;
                case 'CONSIDER REVOKING':
                    riskImage =' <https://res.cloudinary.com/dl4murstw/image/upload/v1683203726/warning_ymvqdc.png>'
                    riskTitle = 'CONSIDER REVOKING'
                    break;
                case 'LOW RISK':
                    riskImage ='<https://res.cloudinary.com/dl4murstw/image/upload/v1683203505/check_zdftsz.png>'
                    riskTitle = 'LOW RISK'
                    break;
                default:
                    riskImage ='<https://res.cloudinary.com/dl4murstw/image/upload/v1683203505/check_zdftsz.png>'
            }

            return (
            <div className='allowanceRow'>

                <div className='risk'>
                    <img className='riskLogo' src={riskImage} title={riskTitle} alt='tokenlogo'/>
                </div>

                <div className='contract'>
                    <div className='contractName'>{item.spender_address_label === null ? "Unknown contract" : item.spender_address_label}</div>
                    <a className='addressUrl' href={'<https://etherscan.io/address/>' + item.spender_address} target='_blank' rel='noreferrer'><div className='contractAddress'><img className='scroll' src='<https://res.cloudinary.com/dl4murstw/image/upload/v1668495918/scroll_bkmays.png>' alt='scroll' width='12px' /> {truncateEthAddress(item.spender_address)}</div></a>
                </div>

                <div className='allowance'>
                {item.allowance === "UNLIMITED" ? "Unlimited" : "$" + item.allowance_quote.toFixed(2)}
                </div>

                <div className='valueAtRisk'>
                {item.pretty_value_at_risk_quote}
                </div>

                <div className='dateApproved'>
                {item.block_signed_at.slice(0,10)}
                </div>

                <div className='revoke'>
                <button className='revokeButton'>Revoke</button>
                </div>
            </div>
            )
        })}
        </>
    )
}

export default Contracts

Again, the code is pretty self-explanatory. Apart from a truncateEthAddress helper function (which helps shorten the standard wallet address), the only customization we’ve done is to render images that correspond to the values in the item.risk_factorfield. If the values are ‘HIGH RISK, REVOKE NOW’, we render a red warning sign. If it’s 'CONSIDER REVOKING', we render a stop sign. If it’s 'LOW RISK', we render a circled tick. The rest of the fields are all rendered with minimal cleaning.

Running npm start, you’ll see the following result:

Awesome! Everything is looking great. However, our Revoke button is not working because we haven’t configured it. Let’s fix that.

6

Adding revoke functionalities

To revoke a particular contract’s approval for a token, we’ll have to call the approve method of the token contract and set the approval to 0 for that particular spender. Sounds confusing? Don’t worry - it’ll be clear once you get to the end of this section.

Let’s install ethers.js to help us with that. First, run the following in the project root:

npm i --save [email protected]

(We’ll install version 5.7 as that is the stable version at the time of writing.)

Then, let’s include this event handler in the <Contracts /> component.

JavaScript
const handleRevoke = async (spenderAddress) => {
        const ethereum = window.ethereum
        const accounts = await ethereum.request({
            method: "eth_requestAccounts"
        })
        const provider = new ethers.providers.Web3Provider(ethereum)
        const walletAddress = accounts[0]

        if (walletAddress !== queryWalletAddress.toLowerCase()) {
            alert(`Sorry, you cannot revoke approval for this wallet address:${queryWalletAddress} as this is not your wallet.`)
            return
        }
        const signer = provider.getSigner(walletAddress)

        const abi = [
            "function name() public view returns (string)",
            "function symbol() public view returns (string)",
            "function decimals() public view returns (uint8)",
            "function totalSupply() public view returns (uint256)",
            "function approve(address _spender, uint256 _value) public returns (bool success)"
        ]

        const tokenContract = new ethers.Contract(tokenAddress, abi, signer)

        await tokenContract.approve(spenderAddress, "0")
        
        alert("Successfully revoked approval!")
    }

This event handler will get your wallet address from Metamask (provided that it’s installed), check that it’s the same address as the one you’ve queried, and if it’s the same, call the approve method for the particular token that is associated with that contract approval.

We will not get into details here, but this is quite a standard way of calling a contract method using ethers.js. You can read more about it here, or here.

Once you click on the revoke button, Metamask will appear and ask for your approval of this transaction. Note: it is a transaction, and there will be gas fees if you press approve!

The full state of the components at this point can be found here.

Amazing. We’re pretty much done with our component.

All in a Day’s Work

Congratulations. You’ve successfully built a component that enables you to check all your token contract approvals for a wallet address, warns you if something seems awry, and also gives you the option to revoke the approval with one click (if it is your wallet). This is extremely useful if you are building a wallet app or a portfolio manager for your clients.

Hopefully, by this point, you’ll be able to see that checking the user’s approvals is really easy with the Covalent API. Currently, the approvals endpoint is supported for Ethereum, BSC, and Polygon, but more chains will be supported very soon.

If you have not been following along and would just like the end state, you can do the following to see this app locally:

git clone https://github.com/xiaogit00/building-wallets.git

cd building-wallets

npm i

git checkout part5-approvals-end

npm start

Just be sure to add your API key in a .env file, like so:

REACT_APP_APIKEY='ckey_8bdxxxxxxxxxxxxxxxxxxxxxxxxxx99'

Happy buidling!

Get your free API key today and give it a spin! Like this how-to guide? Give it a RT on Twitter.

Appendix

*How is value_at_risk calculated?

The value_at_risk field is calculated based on the highest allowance amount set for a particular token, given the amount of token you hold. It is understood simply as: the amount you stand to lose across all spenders. For instance, if you have $5000 in a wallet and two spenders were granted allowances of $200 and $300 respectively, then the value at risk is $500.

**How is risk_factor calculated?

Currently, the risk_factor is calculated based on the value at risk. If the value at risk is over $10,000 USD, then this flag returns "HIGH RISK, REVOKE NOW". If the value is between $100 and $10,000, then it returns "CONSIDER REVOKING". If it is less than $100, it’ll return “LOW RISK”.