Skip to main content

Creating a RESTful API for Compound Finance Smart Contracts

Created on
Updated on
Dec 17, 2024

11 min read

Overview

Compound finance are early pioneers in the decentralized finance space, as one of the first defi lenders. Compound offers a way to earn interest on several tokens: ETH, BAT, DAI, REP, WBTC, USDC & a few others. Compound makes this possible by locking your assets in a smart contract on the Ethereum chain that pays out interest every single time a block is mined (about once every 15 seconds). 

If you are an exchange or a wallet, you might want to offer your users a way to lock their assets in Compound in order to earn interest while they're not trading. In this guide, we will develop a simple REST API that supports all available compound assets with most Compound smart contract methods that allow you to earn interest - using Python, Flask and Web3.py.

Understanding Compound Lending Platform

There is some Compound specific vocabulary we need to understand before we go about building our API. This is not an all encompassing glossary, if you'd like more in depth explanations, go check out the compound finance documentation on cTokens, but here is the basic terminology to get us started:

cTokens:
So cTokens are basically tokens that you get in exchange for other tokens, like ETH, BAT, WBTC, REP, DAI, USDC, USDT and ZRX. cTokens pay you interest based on the market rate. Compound is constantly adding new cTokens and you can see a list here.

cTokens are technically ERC-20 tokens themselves.

Minting:
Minting is how you loan out your tokens and get cTokens in return. You send your desired asset in, and get cTokens back. For example, you can send USDC to a Compound smart contract and get back cUSDC - which at the time of this writing, earns an APY of 1.24%.

Redeeming:
Redeeming is how you request assets back from your account and get part or all of your original asset back + interest. So for example, if I "minted" 500 cUSDC by sending in 500 USDC and it grew to 550 cUSDC, I could make a call to redeem all of my cUSDC balance and receive 550 USDC back.

Note: For our purposes we will only code an API against viewing balances, viewing interest rates, minting aka supplying and redeeming your tokens. Also, we did not use real cToken exchange rates to keep the examples simple.

Borrowing:
Borrowing is exactly how it sounds, except you can only borrow if you have cTokens. For each cToken type, there is a maximum percent of your collateral that you can borrow. For instance, for cETH, you can borrow up to a maximum of 75% of your cTokens. To make it more practical, imagine if you used 100 ETH to mint cETH. Then you could only borrow 75 ETH or it's equivalent value in assets supported by the Compound protocol / smart contracts. Once you borrow, interest starts accumulating after each block is mined based on the borrowing rates set by Compound.

Repaying:
This is the last piece. This is of course, how you pay back the loan you borrowed. You will need to repay the amount you borrowed plus interest. In order to find out how much you owe at any given moment, you need to ask the Compound smart contract.

Environment set up

Today, we'll be building with Python 3. So, please make sure you have that installed. Let's first create a directory to house all of our work:

$ mkdir compound-rest && cd compound-rest

Now let's set up a lightweight virtual environment to keep our system python clean:

$ python3 -m venv compound_env && source compound_env/bin/activate

You might need to add an extension to your `bin/activate` in the example above if you're using a different shell, like for instance, fish. Go here to find your extension. By now, this is about what your terminal should look like:

Last step will be to add a `requirements.txt` file and install all of the required software, create a new file called `requirements.txt` in your `compound-rest` directory and put this in there:

flask
web3
requests

Now go ahead and install them:

$ pip3 install -r requirements.txt

Setting Up a QuickNode Ethereum Endpoint

You will need to either install and sync your own Ethereum node, or use a service like QuickNode to get access to the ethereum blockchain. If you'd like to sync your own node, we highly recommend using EthHub as a jumping off point.

If you'd like to skip all that, you can create a free QuickNode account and get an endpoint here. Return to the guide once you've created an endpoint and copied the HTTP Provider like in this screenshot:

Important: Save this URL we'll need it later.

Fetching supported tokens

Next let's get a small script going to fetch all the supported currencies you can earn interest on from Compound. They have this handy endpoint we can use: https://api.compound.finance/api/v2/ctoken

Here is the script to start, I put it in `supported_tokens.py`:

import requests
import pprint
res = requests.get("https://api.compound.finance/api/v2/ctoken")
pp = pprint.PrettyPrinter(indent=4)
pp.pprint(res.json())

Go ahead and run this:

$ python3 supported_tokens.py

If you see a bunch of output that contains `cToken` - you should be good. It should look like this:

We will use this to make sure our REST API dynamically supports new assets as they become supported by Compound without us having to update our code each time. For reference, if you would like official documentation on what we're looking at and using, check out the Compound documentation.

Creating a wallet for our API

Before we proceed, we'll need to piece together a little script to generate an Ethereum wallet, with it's private key and public address. If you have your own, you can use that, but we caution against that. Put the following in a file called `create_address.py`:

from web3.auto import w3
acct = w3.eth.account.create()
print("Private Key **SAVE THIS**: {}".format(acct.privateKey.hex()))
print("ETH address: {}".format(acct.address))

Save the private key and address we will need them in our next step.

Creating a REST API for Compound with Flask

So here is the meat of our app, all of our code here will go into a file called `app.py` in the `compound-rest` folder we created earlier:

import os
import json
import requests
from decimal import *
from flask import Flask, request
from web3 import Web3
app = Flask(__name__)

ETH_PROVIDER = os.environ.get('ETH_PROVIDER')
ETH_ACCT_KEY = os.environ.get('ETH_ACCT_KEY')
ETH_ADDRESS = os.environ.get('ETH_ADDR')
w3 = Web3(Web3.HTTPProvider(ETH_PROVIDER))

@app.route('/ctokens')
def ctokens():
"""
Returns all ctokens:
- symbol
- symbol on compound
- interest rate
"""
res = requests.get("https://api.compound.finance/api/v2/ctoken")
response = []
for t in res.json()['cToken']:
token = {
'symbol': t['underlying_symbol'],
'compound_symbol': t['symbol'],
'rate': '{}%'.format(round(Decimal(t['supply_rate']['value'])*100, 2))
}
response.append(token)
return ({'supported': response}, {'Content-Type': 'application/json'})

@app.route('/ctokens/<symbol>')
def ctokens_detail(symbol):
"""
Returns particular ctoken for underlying:
- symbol
- interest accrued
- principal
- current ctoken balance
"""
symbol = symbol.upper()
tokens = requests.get("https://api.compound.finance/api/v2/ctoken")
balances = requests.get("https://api.compound.finance/api/v2/account?addresses[]={}".format(ETH_ADDRESS))

response = {
'principal': '0.0',
'current_balance': '0.0',
'interest_accrued': '0.0',
'ctoken_balance': '0.0'
}
for t in tokens.json()['cToken']:
if t['underlying_symbol'] == symbol:
response['symbol'] = t['underlying_symbol']
accts = balances.json()['accounts']
if len(accts) > 0:
for acct in accts[0]['tokens']:
if acct['symbol'] == t['symbol']:
abi_url = "https://raw.githubusercontent.com/compound-finance/compound-protocol/master/networks/mainnet-abi.json"
abi = requests.get(abi_url)
tokens = requests.get("https://api.compound.finance/api/v2/ctoken")
contract_address = [t['token_address'] for t in tokens.json()['cToken'] if t['underlying_symbol'] == symbol][0]
compound_token_contract = w3.eth.contract(abi=abi.json()["c{}".format(symbol)], address=Web3.toChecksumAddress(contract_address))
nonce = w3.eth.getTransactionCount(ETH_ADDRESS)
total = acct['safe_withdraw_amount_underlying']['value']
interest = acct['lifetime_supply_interest_accrued']['value']
response['principal'] = "{}".format(Decimal(total) - Decimal(interest))
response['current_balance'] = total
response['interest_accrued'] = interest
response['ctoken_balance'] = str(compound_token_contract.functions.balanceOf(ETH_ADDRESS).call())
return (response, {'Content-Type': 'application/json'})

@app.route('/ctokens/<symbol>/mint', methods=['POST'])
def ctokens_mint(symbol):
"""
Mints particular ctoken based on:
- symbol
- amount
and returns:
- symbol
- amount minted
- tx id
"""
symbol = symbol.upper()
amt = request.form['amount']
abi_url = "https://raw.githubusercontent.com/compound-finance/compound-protocol/master/networks/mainnet-abi.json"
abi = requests.get(abi_url)
tokens = requests.get("https://api.compound.finance/api/v2/ctoken")
contract_address = [t['token_address'] for t in tokens.json()['cToken'] if t['underlying_symbol'] == symbol][0]
amount = w3.toWei(amt, 'ether')
compound_token_contract = w3.eth.contract(abi=abi.json()["c{}".format(symbol)], address=Web3.toChecksumAddress(contract_address))
nonce = w3.eth.getTransactionCount(ETH_ADDRESS)
mint_tx = compound_token_contract.functions.mint().buildTransaction({
'chainId': 1,
'gas': 500000,
'gasPrice': w3.toWei('20', 'gwei'),
'nonce': nonce,
'value': int(amount)
})
signed_txn = w3.eth.account.sign_transaction(mint_tx, ETH_ACCT_KEY)
try:
tx = w3.eth.sendRawTransaction(signed_txn.rawTransaction)
except ValueError as err:
return (json.loads(str(err).replace("'", '"')), 402, {'Content-Type': 'application/json'})

response = {
'symbol': symbol,
'amount': amt,
'tx_id': tx.hex()
}
return (response, {'Content-Type': 'application/json'})

@app.route('/ctokens/<symbol>/redeem', methods=['POST'])
def ctokens_redeem(symbol):
"""
Redeems particular ctoken based on:
- symbol
- amount
and returns:
- symbol
- tx_id
"""
symbol = symbol.upper()
amt = request.form['amount']
abi_url = "https://raw.githubusercontent.com/compound-finance/compound-protocol/master/networks/mainnet-abi.json"
abi = requests.get(abi_url)
tokens = requests.get("https://api.compound.finance/api/v2/ctoken")
contract_address = [t['token_address'] for t in tokens.json()['cToken'] if t['underlying_symbol'] == symbol][0]
compound_token_contract = w3.eth.contract(abi=abi.json()["c{}".format(symbol)], address=Web3.toChecksumAddress(contract_address))
nonce = w3.eth.getTransactionCount(ETH_ADDRESS)
redeem_tx = compound_token_contract.functions.redeem(int(amt)).buildTransaction({
'chainId': 1,
'gas': 500000,
'gasPrice': w3.toWei('20', 'gwei'),
'nonce': nonce
})
signed_txn = w3.eth.account.sign_transaction(redeem_tx, ETH_ACCT_KEY)
try:
tx = w3.eth.sendRawTransaction(signed_txn.rawTransaction)
except ValueError as err:
return (json.loads(str(err).replace("'", '"')), 402, {'Content-Type': 'application/json'})

response = {
'symbol': symbol,
'tx_id': tx.hex()
}
return (response, {'Content-Type': 'application/json'})

Save that file. Now let's run our server:

$ ETH_ADDR=<WALLET_ADDRESS> ETH_ACCT_KEY=<WALLET_PRIVATE_KEY> ETH_PROVIDER=<ETHEREUM_NODE_PROVIDER_HERE> FLASK_APP=app.py flask run

Remember to replace the parts that say "<WALLET_ADDRESS>", "<WALLET_PRIVATE_KEY>" and "<ETHEREUM_NODE_PROVIDER_HERE>" with the appropriate values from earlier sections. Once you have your server running, go ahead and fund that wallet with the assets of your choice, plus some ETH for transaction costs and you're good to go. Here are a few screenshots of cURL requests to localhost:

Listing all supported Compound assets via API:

Deploying

So for our guide, we won't deploy this app to production, but we highly recommend this Digital Ocean tutorial on deploying flask apps to production and this other tutorial on securing apps with Lets Encrypt, the free SSL provider for the web.

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide