Building Web3 Wallets (Part 5) - Revoking Approvals for Your Wallet
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.
GoldRush's Token Approvals API Endpoint
However, there is no easy way to check which contracts you’ve approved to spend your tokens. Luckily, with GoldRush’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
- 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.
2
Examining the data
https://api.covalenthq.com/v1/1/approvals/{walletAddress}/?key={apiKey}
walletAddress
and apiKey
variables, and pop the endpoint into the browser. If you don’t have a GoldRush API key, you can get it for free here.{
"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
}
token_address_label
), token ticker (ticker_symbol
), token balance (balance
), etc. All these are useful for rendering the first row in our example above.spenders
attribute (which I’ve collapsed in the example above). Here is what an uncollapsed spenders array looks like:"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"
}
]
- Contract name -
spender_address_label
- Contract address -
spender_address
- Allowance amount -
allowance
orpretty_allowance_quote
- Value at risk -
value_at_risk
* orpretty_value_at_risk_quote
- Date approved -
block_signed_at
- Risk factor -
risk_factor
**
value_at_risk
and risk_factor
are calculated, see appendix.)3
Clone this starter kit & initialize the project
<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.App.css
.<TokenAllowance />
component.4
Fetching the approvals data
npm start
and opening up the dev console, you should be able to see the following:5
Rendering the data
<TokenAllowance />
component.<TokenAllowance />
component:<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).<Token />
component is as follows:<Contracts />
component: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
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_factor
field. 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.npm start
, you’ll see the following result:6
Adding revoke functionalities
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.npm i --save [email protected]
<Contracts />
component.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!")
}
approve
method for the particular token that is associated with that contract approval.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 GoldRush 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”.