Building a NestJS API to Extract Prices from Thena Protocol

Burgossrodrigo
CoinsBench
Published in
4 min readMay 2, 2024

--

This tutorial will guide you through setting up a NestJS API that interacts with Ethereum-based contracts to fetch and process token prices from the Thena protocol. By the end, you’ll have a service that can retrieve and compute the prices of tokens in a given liquidity pool.

Prerequisites

  • Basic understanding of TypeScript and Node.js.
  • Familiarity with Ethereum blockchain concepts like smart contracts.
  • Node.js and NestJS installed on your machine.
  • An Ethereum wallet with some test ETH for deploying contracts (optional).
  • Familarity with typechain cli

Setup

Create a new NestJS project if you haven’t already:

nest new thena-price-api

Navigate into your project directory:

cd thena-price-api

You will need ethers.js to interact with Ethereum blockchain:

npm install ethers

Install additional TypeScript definitions for development:

npm install --save-dev @types/ethers

Install also typechain and typechain ethers-v6 targer:

npm install --save-dev typechain && @typechain/ethers-v6

Ethereum Service

You can find how to build the ethereum service here:

https://medium.com/coinsbench/automated-test-for-ethereum-service-on-nestjs-a217a1676807

ERC20 Service:

First we will need the ERC20 abi, you can find it in this gist:

https://gist.github.com/veox/8800debbf56e24718f9f483e1e40c35c

Right after i need to save the content of the file in a folder named abi, inside the folder from erc20 service:

In the root folder of the nest app i need you to run the following command:

npx typechain --target=ethers-v6

The output should be this:

Successfully generated n typings!

Then we proceed to the code. The general aspect of the class should be somthing like this:

@Injectable()
export class Erc20Service { ... }

Our first method should be this one, getTokenInstance:

    getTokenInstance = async (tokenAddress: string, chainId: number): Promise<ERC20> => {
return ERC20__factory.connect(
tokenAddress,
await this.ethereumService.chooseRpc(chainId)
)
}

The ERC20__factory were generated by typechain, it includes a couple of methods, but the one that is of our interest at the moment is the connect(). which takes two arguments, the address of the contract aka address of the token and an runner, that for our purpose means an json rpc provider (but would required an wallet if we are planning to sign transactions).

Our second method, getTokenData():

    public async getTokenData(tokenAddress: string, chainId: number): Promise<ITokenData> {
try {
const token = await this.getTokenInstance(tokenAddress, chainId)
const [decimals, symbol, name] = await Promise.all([
token.decimals(),
token.symbol(),
token.name()
])
return { decimals, symbol, name }
} catch (error) {
throw new Error(`getTokenData: ${error.message}`)
}
}

It’s a generic method to extract sensible data from a ERC20 smartcontract. These are decimals, symbol and name. Let’s hold with this for now.

We also need two methods to adapt decimals from the smart contract when we are extracting decimals data and to the smart contract when we are sending transactions:

    public formatUnitsFrom(amount: number, decimals: number): number {
return amount / Math.pow(10, decimals)
}

public formatUnitsTo(amount: number, decimals: number): number {
return amount * Math.pow(10, decimals)
}

THENA Service:

We will need to reproduce the same process with the ERC20 abi to generate types with typechain. Here goes the ABI:

Copy the content of this gist in an Pool.abi file and save it inside the folder from the Thena service.

Again, execute this line on the root of the nestjs:

npx typechain --target=ethers-v6

The output:

Successfully generated ntypings!

First let’s declare the constructor of our class import the additional services:

    constructor(
public readonly ethereumService: EthereumService,
public readonly erc20Service: Erc20Service
) { }

Then we proceed to instanciate the contract we want to deal with using the class generated with typechain:

    public getPoolInstance = async (poolAddress: string, chainId: number): Promise<Pool> => {
return Pool__factory.connect(
poolAddress,
await this.ethereumService.chooseRpc(chainId)
)
}

The main method to extract the pool data:

    public async getPoolData(poolAddress: string, chainId: number): Promise<IRawPoolData> {
try {
const pool = await this.getPoolInstance(poolAddress, chainId)

const [reserves, token0, token1] = await Promise.all([
pool.getReserves(),
pool.token0(),
pool.token1()
])

return { token0: token0, token1: token1, reserves: reserves }

} catch (error) {
throw new Error(`getPoolData: ${error.message}`)
}
}

A second method to process raw data to a more human redable format:

    public async processPoolData(poolAddress: string, chainId: number): Promise<IPoolData> {
try {
const { token0, token1, reserves } = await this.getPoolData(poolAddress, chainId)
console.log(reserves._reserve0, reserves._reserve1, 'here')
const token0Data = await this.ethereumService.getTokenData(chainId, token0)
const token1Data = await this.ethereumService.getTokenData(chainId, token1)
const price0 = this.erc20Service.formatUnitsFrom(Number(reserves._reserve0), Number(token0Data.decimals)) / this.erc20Service.formatUnitsFrom(Number(reserves._reserve1), Number(token1Data.decimals))
const price1 = 1 / price0
return { token0, token1, price0, price1 }
} catch (error) {
throw new Error(`processPoolData: ${error.message}`)
}
}

And last, a controller to deliver the data from the api:

  @Post('/get-pool-data')
@ApiOperation({ summary: 'Get raw pool data from the blockchain' })
@ApiResponse({ status: 200, description: 'The pool data', type: PoolDataResponseDto })
@UsePipes(new ValidationPipe({ transform: true }))
async getPoolData(
@Body('poolAddress', ValidateEthereumAddressPipe) poolAddress: string,
@Body() poolDataRequestDto: PoolDataRequestDto
): Promise<PoolDataResponseDto> {
const { chainId } = poolDataRequestDto;
const data = await this.thenaService.processPoolData(poolAddress, chainId);
return {
token0: data.token0,
token1: data.token1,
price0: data.price0,
price1: data.price1,
};
}

This way you will be able to interact with the api through swagger:

That’s all folk. Obviously i will not deliver everything thus you will work on it, try for yourselves and overcome the challenges that may appear but fell free to ask me for help :)

--

--