Los exchanges descentralizados (DEXs) generan enormes cantidades de datos que son interesantes de leer pero difíciles de recopilar.
Afortunadamente, TheGraph ofrece la posibilidad de obtener la cantidad mínima requerida de datos que se necesitan de una manera simple utilizando GraphQL.
En esta publicación intentaré explicar cómo desarrollar una aplicación creada con Vite.js que consulte los subgraphs de Uniswap v2 y v3 en TheGraph, procese los datos, los almacene en un estado global y los represente utilizando componentes de React.js.
En este artículo
Enfoque
Para abordar el desarrollo de esta aplicación, que puede consistir en un número creciente de exchanges, he adoptado una arquitectura limpia adaptada al front end.
Mi implementación se basa en una arquitectura hexagonal en la que el estado existe fuera del núcleo y persiste globalmente utilizando Redux toolkit.
El núcleo, o core, es donde se define la lógica de negocio, esto es, entidades, repositorios, casos de uso y adaptadores.
He pensado en cada DEX compatible con la aplicación como una feature la cuál consta de su core, fuentes de datos, un slice de estado de Redux y componentes de interfaz de usuario.
Por lo tanto, si la aplicación fuera a escalar, dar soporte a un nuevo exchange descentralizado consistiría aproximádamente en agregar una nueva carpeta con código independiente en src/features.
Inversamente, eliminar un exchange de la aplicación consistiría aproximádamente en remover la carpeta correspondiente, sin afectar el resto de la lógica.
Este enfoque también garantiza un desacoplamiento completo de la lógica de negocio, la infraestructura, la interfaz de usuario y el estado de la aplicación.
En otras palabras, la lógica de negocio no está afectada por los requisitos de las fuentes de datos, la transición a un framework o librería de front end diferente sería trivial y la lógica de estado podría ser facilmente reutilizada.
Obtención de datos
Esta aplicación utiliza renderizado del lado del cliente (client side rendering o CSR) para obtener y renderizar pools y/o tokens.
Por ejemplo, para obtener el top 50 de tokens en Uniswap v3, esta app renderiza un componente de React.js con el hookuseTokensUniswapV3, el cuál activará dos casos de uso o interactors:
Cada interactor llama a un método diferente de un repositorio y estos métodos se implementan en clases de TypeScript, que representan fuentes de datos.
queryBlocksEthereum conformará una entidad Blocks con números de marcas de tiempo, en segundos, y números de bloques para cuatro marcas de tiempo: current, t1D, t2D y t1W.
queryTokensAndPricesUniswapV3 utilizará estos números de bloques para conformar una entidad TokensAndPrices con:
50 tokens ordenados por totalValueLockedUSD para cada marca de tiempo.
precios de Ether para las mencionadas marcas de tiempo, los cuales se utilizarán para calcular conversiones de ETH a USD.
La lógica para consultar los números de bloques y las marcas de tiempo se implementa junto con otro código en la carpeta compartida en el nivel de la carpeta de features, ya que otros protocolos compartirán este código.
En la siguiente sección explicaré en detalle los componentes que he mencionado.
Estrucutra de los archivos
La estructura de este repositorio se resume a continuación:
Este archivo renderizará el archivo main.tsx el cuál contiene el método ReactDOM.render():
src/main.tsx
Este método renderiza la aplicación en el navegador. El componente App.tsx está en la carpeta src/app, que junto con src/features, ambas contienen la mayor parte del código.
Desarrollaré el contenido de estas dos carpetas importantes a continuación.
App
src/app reúne la lógica que no está sujeta a una feature en particular sino a la aplicación en sí. Contiene el componente App.tsx, el manager de rutas, el store de Redux, estilos globales, componentes de maquetación o diseño (layout), etc
A continuación podemos ver el código del componente App.tsx:
src/app/ui/App.tsx
App.tsx renderiza el componente RouteManager.tsx, el cual define todas las rutas admitidas por la aplicación.
Por ejemplo, para continuar con la tarea de obtener tokens en Uniswap v3, la ruta /ethereum/uniswap-v3/tokens se ajusta a la ruta que renderiza el componente Tokens.tsx, como podemos ver a continuación:
src/app/ui/routes/RouteManager.tsx
Tokens.tsx renderiza los tokens del protocolo que figure en la URL, siempre y cuando la aplicación le de soporte.
Features
Como dijimos, DEXs Analytics consiste de un número de features (protocolos de exchanges descentralizados), y todos ellos tienen la misma estructura de archivos.
A continuación podemos ver la estructura de archivo de Uniswap v3:
En las siguientes secciones explicaré estos archivos y me referiré, de nuevo, a la tarea de la obtención del top 50 de tokens en Uniswap v3, para poder ver parte del código.
Core
Esta carpeta reúne la lógica de negocio de la aplicación. Consiste de entidades, repositorios, interactores y adaptadores.
Los interactores interactuan con los repositorios para obtener entidades. Estas entidades, entonces, se pasan a los adaptadores los cuáles devuelven otras entidades que son comunes al resto de protocolos.
Todo el código en el núcleo (core) es indepediente de la infraestructura.
Entidades
La interfaz que representa el objeto con tokens devuelto por el subgraph de Uniswap v3 se puede ver a continuación:
Podemos inferir a partir de esta entidad que en una misma consulta se solicitan tokens y precios de ether para diferentes marcas de tiempo.
También lo podemos comprobar en la implementación del método getTokensAndPricesByBlocks en fuentes de datos.
Repositorios
Un repositorio es una interfaz que describe los métodos que se requieren. En el caso de UniswapV3Repository, reúne 3 métodos incluyendo getTokensAndPricesByBlocks.
Los interactores son los casos de uso de sus respectivas features (o protocólos, en el caso que nos ocupa) .
Por ejemplo, el caso de uso para generar una entidad TokensAndPrices en Uniswap v3 es queryTokensAndPricesUniswapV3.
queryTokensAndPricesUniswapV3 obtiene un endpoint y un objeto Blocks y los pasa al método getTokensAndPricesByBlocks en el repositorio UniswapV3Repository.
La implementación de este método devuelve una promesa de un objeto con la interfaz TokensAndPricesUniswapV3 que se resuelve y, posteriormente, se adapta a la interfaz Tokens.
Aquí se podría crear una instancia de UniswapV3DataSource. Sin embargo, para que nuestra lógica no se vea afectada por ningún cambio en la infraestructura, obtiene una abstracción (un repositorio UniswapV3Repository) en lugar de depender de una implementación de fuente de datos específica.
Por esta razón, el interactor que se importará en nuestra interfaz de usuario tendrá la dependencia -una instancia a UniswapV3DataSource- ya inyectada:
Los adpatadores son funciones que convierten los objetos recibidos por los subgraphs en TheGraph en objetos con interfaces comunes a todos los protocolos.
Esto asegura que los componentes que renderizan datos siempre obtengan objetos con la misma interfaz, sin importar el protocolo.
Por lo tanto, un adaptador puede ayudar como "barrera" en caso de que haya un cambio en el esquema de GraphQL del subgraph, ya que los únicos campos a cambiar serían los de la interfaz recibida, y esto no afectaría necesariamente al resto del código.
Fuentes de datos
Las fuentes de datos son clases de TypeScript que implementan un repositorio de un protocolo.
En el caso de Uniswap v3, UniswapV3Repository requiere la implementación de tres métodos. Uno de ellos es getTokensAndPricesByBlocks, el cuál se utiliza para la obtención de tokens y precios de ether.
Para ello, consulta el subgraph de Uniswap v3 con un instancia de un GraphQLClient y devuelve una promesa de un objeto con la interfaz TokensAndPricesUniswapV3.
Un objeto de Blocks se pasa a getTokensAndPricesByBlocks porque necesitamos consultar cuatro marcas de tiempo diferentes, y esto se logra pasando el número de bloque como argumento en cada entidad en la consulta.
Sin embargo, en lugar de consultar el subgraph ocho veces, una vez por cada marca de tiempo (current, t1D, t2D y t1W) para cada entidad (tokens y precios de ether), creamos una única consulta:
"..." en las líneas 12 y 19 representa código que se ha borrado por su longitud.
El argumento id_not_in, el cuál recibe una lista de identificadores, se utiliza para descartar ciertos tokens.
Estado
El estado de cada feature se reduce a un conjunto de slices de Redux Toolkit, las cuales se sitúan fuera del núcleo de la feature.
Siguiendo el ejemplo anterior, el estado de los tokens de Uniswap v3 viene dado por un slice de Redux toolkit con solo un reductor: setTokensUniswapV3.
setTokensUniswapV3 obtendrá un TokenState como payload y actualizará el estado.
TokenState es una interfaz con unos campos de control loading y error, y con el campo data, el cuál tiene como índice la interfaz de Record con la red de la cadena de bloques que se seleccione, y como valor un objeto con dos campos: tokens y lastUpdated.
Como podemos ver, los tokens para diferentes marcas de tiempo de las redes donde se desplieguen los protocolos se indexarán en un objeto Record junto con otros datos.
La idea es que los datos de todos los protocolos coexistan en sus respectivos slices de estado y persistan en un índice del estado.
De esta manera, se evitan consultas cada vez que los datos vayan a ser renderizados, a menos que haya pasado una cantidad de tiempo específica, que actualmente es 15 minutos.
Por lo tanto, los estados de todos los tokens y pools se definen en sus slices y se pasan a la store de Redux.
src/app/state/store.ts
Interfaz de usuario
La carpeta ui reúne la implementación de los hooks y los componentes de React.js. Estos últimos obtienen tokens y pools de los hooks y renderizan otros componentes con tablas y la paginación.
En nuestro ejemplo de tokens, useTokensUniswapV3 es el hook que asigna el estado en el slice de Redux correspondiente enviando la payload adecuada a setTokensUniswapV3, dependiendo de las respuestas de cada interactor que se ejecuta.
En última instancia, este estado controlará qué mostrar en la tabla de tokens, es decir, un mensaje de carga, un mensaje de error o los tokens mismos.
En este hook, usamos la función getFormattedTokensUniswapV3 para editar algunos campos y crear otros, es decir, los cambios diarios, de dos días y semanales.
Además, con un useEffect controlamos que la función callbackfetchData solo se llame si los tokens aún no se han obtenido, o si se han obtenido hace más de 15 minutos.
Por lo tanto, aprovechando la persistencia del estado en todas las rutas de la aplicación, mi intención es tener una buena transición entre las rutas de la aplicación -sin demoras- una vez que se hayan obtenido los tokens, pools o pairs de todas las rutas que se hayan consultado, incluyendo los de otras redes donde opere un protocolo.
Podemos ver a continuación la implementación de TokensUniswapV3, donde se invoca el hook para obtener los tokens:
src/features/uniswapV3/ui/TokensUniswapV3.tsx
TokensUniswapV3 renderiza también el componente TokensTablePagination, el cuál es compartido con el resto de los protocols.
TokensTablePagination muestra una tabla con los tokens almacenados en el estado, siempre y cuando se hayan obtenido con éxito.
Si no es así, se muestra un mensaje cuyo contenido depende de los campos de control en el estado y de los datos memoizados:
Introducción a una serie de artículos acerca de Olive Oil Trust
¿Preparado para #buidl?
¿Está interesado en Web3 o en las sinergias entre la tecnología blockchain, la inteligencia artificial y el conocimiento cero?. Entonces, no dude en contactarme por e-mail o en mi perfil de LinkedIn. También me puede encontrar en GitHub.