Decentralized exchanges (DEXs) generate enormous quantities of data which are interesting to read but difficult to gather.
Fortunately, TheGraph offers the possibility to obtain the minimum required amount of data that are needed in a simple manner using GraphQL.
In this post I'll try to explain how to develop an application built with Vite.js that queries Uniswap v2 and v3 subgraphs in TheGraph, processes the data, stores them in a global state and represents them using React.js components.
In this article
Approach
In order to address the development of this application, which may consist of an increasing number of exchanges, I've adopted a clean architecture adapted to the front end.
My implementation is based on a hexagonal architecture in which the state exists outside the core and persists globally using Redux toolkit.
The core is where the business logic is defined, this is, entities, repositories, use cases and adapters.
I thought of every DEX supported by the application as a feature that consists of its core, data sources, a Redux state slice and UI components.
Therefore, if the application was to escalate, supporting a new decentralised exchange would consist approximately of adding a new folder with independent code in src/features.
Inversely, removing an exchange from the app would consist approximately of getting rid of its corresponding feature, without affecting the rest of the logic.
This approach also grants a complete decoupling of the business logic, the infrastructure, the user interface and the state of the application.
In other words, the business logic is not affected by the data sources requirements, transitioning to a different front-end framework or library would be trivial and the state logic could be easily reused.
Data fetching
This application uses client side rendering (CSR) to fetch data and render pools and/or tokens.
For instance, to get the top 50 tokens in Uniswap v3, this app renders a React.js component with the hook useTokensUniswapV3, which will trigger two use cases or interactors:
Every interactor calls a different method of a repository and these methods are implemented in TypeScript classes, which represent data sources.
Each method will query a TheGraph subgraph with a GraphQL client and will return an object which, subsequently, is adapted to an interface common to all protocols.
queryBlocksEthereum will conform a Blocks entity with timestamps numbers, in seconds, and block numbers for four timestamps: current, t1D, t2D and t1W.
queryTokensAndPricesUniswapV3 will use these block numbers to conform an entity TokensAndPrices with:
50 tokens ordered by totalValueLockedUSD for every timestamp.
ether prices for the mentioned timestamps, that are used to calculate ETH to USD conversions.
The logic to query the block numbers and timestamps is implemented along other code in the shared folder at the features folder level, since other protocols will share these code.
In the following section I'll go over the components that I just mentioned, in detail.
File structure
The structure of this repository is summarized below:
This will render render main.tsx which contains the method ReactDOM.render():
src/main.tsx
This method renders the app in the browser. The App.tsx component is in the folder src/app, which along src/features, both contain the bulk of the code.
I will explain the content of these two important folders below.
App
src/app gathers the logic that is not subject to a particular feature but to the application itself. It contains the App.tsx component, the route manager, the Redux store, global styles, layout components, etc
We can see below the code of the component App.tsx:
src/app/ui/App.tsx
App.tsx renders the RouteManager.tsx component, which defines all the supported paths in the application.
For instance, to follow with the task of retrieving Uniswap v3 tokens, the path /ethereum/uniswap-v3/tokens matches the path that renders the Tokens.tsx component, as we can see below:
src/app/ui/routes/RouteManager.tsx
Tokens.tsx renders the tokens of the protocol that is given in the path, as long as the app supports it.
Features
As we said, DEXs Analytics consists of a number of features (DEX protocols), and they all have the same file structure.
The file structure of Uniswap v3 can be seen below:
In the following sections I will go over these folders and I will refer again to the task of getting the top 50 tokens in Uniswap v3, so that we can see some of the code.
Core
This folder gathers the business logic of the application. It consists of entities, repositories, interactors and adapters.
Interactors interact with repositories to get entities. Then, these entities are passed to adapters which return other entities that are common to the rest of the protocols.
All the code within the core is independent from the infrastructure.
Entities
The interface that represents the tokens object returned by the Uniswap v3 subgraph can be seen below:
We can infer from this entity that tokens and ether prices for different timestamps are be requested in the same query.
We can also check this in the implementation of the getTokensAndPricesByBlocks method in data sources.
Repositories
A repository is an interface that describes the methods that are required. In the case of UniswapV3Repository, it gathers 3 methods including getTokensAndPricesByBlocks.
Interactors are the use cases of their respective features.
For instance, the use case to generate a TokensAndPrices entity in Uniswap v3 is queryTokensAndPricesUniswapV3.
queryTokensAndPricesUniswapV3 gets an endpoint and a Blocks object and passes them to the getTokensAndPricesByBlocks method in the repository UniswapV3Repository.
The implementation of this method returns a promise of an object with the interface TokensAndPricesUniswapV3 which is resolved here and adapted to the interface Tokens thereafter.
An instance of UniswapV3DataSource could be created here. However, it gets an abstraction (a UniswapV3Repository repository) instead of depending on a specific data source implementation in order for our logic not to be affected by any change in the infrastructure.
For this reason, the interactor that will be imported in our user interface will have the dependency -an instance to UniswapV3DataSource- already injected:
Therefore, an adapter may help as a "barrier" in case there was a change on the subgraph GraphQL schema, as the only fields to be changed would be in the interface received, and this would not necessarily affect the rest of the code.
Data sources
Data sources are TypeScript classes that implement a protocol repository.
In the case of Uniswap v3, UniswapV3Repository requires three methods to be implemented. One of them isgetTokensAndPricesByBlocks, which is used to retrieve tokens and ether prices.
It queries the Uniswap V3 subgraph with n instance of a GraphQLClient and returns a promise of an object with the interface TokensAndPricesUniswapV3.
A Blocks object is passed to getTokensAndPricesByBlocks because we need to query for four different timestamps, and this is accomplished by setting the block number as an argument in every entity in the query.
However, instead of querying the subgraph eight times, once for every timestamp (current, t1D, t2D and t1W) for every entity (tokens and ether prices), there is a single query:
TokenState is an interface with a loading and error control fields, and the field data, which has the interface of Record with the selected blockchain network as index key, and an object with two fields as value: tokens and lastUpdated.
As we can see the tokens for different timestamps of a network that a protocol is deployed will be indexed in a Record object alongside with other data.
The idea is that data of all protocols and networks coexist in their respective state slices and persist in an index of that state
This avoids making queries every time that data are to be rendered unless an specific amount of time has passed which currently is 15 minutes.
Therefore, the states of all the tokens and pools are defined in their slices and passed to the Redux store.
src/app/state/store.ts
User interface
The ui folder gathers both the implementation of the hooks and the React.js components. These components get tokens and pools from the hooks and render other components with tables and pagination.
In our tokens example, useTokensUniswapV3 is the hook that sets the tokens state by dispatching the proper payload to setTokensUniswapV3, depending on the responses of every interactor that is called.
That state will ultimately control what to render in the tokens table, i.e. either a loading message, an error message or the tokens themselves.
In this hook we use the helper function getFormattedTokensUniswapV3 to edit some fields and create others, namely the daily, two-day and weekly changes.
Also with a useEffect we control that the fetchData callback function is only called if the tokens have not been fetched already, or if they have been fetched more than 15 mins ago.
Therefore, leveraging the persistence of the state in all the app routes I aim to have a smooth transition between pages -with no delays- once the tokens, pools or pairs, of all routes have been fetched, including the ones in other networks where a protocol operates.
We can see below the implementation of TokensUniswapV3 where the hook to get the tokens is called:
src/features/uniswapV3/ui/TokensUniswapV3.tsx
TokensUniswapV3 also renders the component TokensTablePagination, which is shared with the rest of the protocols.
TokensTablePagination displays a table with the data stored in the state, as long as it is succesfully retrieved.
Otherwise, a message is shown whose content depends on the control fields in the state and the memoized data:
Next.js application that gives support to members and customers in Olive Oil Trust, and reduces...
Ready to #buidl?
Are you interested in Web3 or the synergies between blockchain technology, artificial intelligence and zero knowledge?. Then, do not hesitate to contact me by e-mail or on my LinkedIn profile. You can also find me on GitHub.