From fc21e5def2716892b7c89fa75c28064554f0f8b6 Mon Sep 17 00:00:00 2001 From: godlin Date: Fri, 15 Sep 2023 20:21:56 +0200 Subject: [PATCH] Page has been reorganized with the addition of new content --- .../quickstart_chains/ethereum-uniswap.md | 1005 +++++++++++++++-- 1 file changed, 926 insertions(+), 79 deletions(-) diff --git a/docs/quickstart/quickstart_chains/ethereum-uniswap.md b/docs/quickstart/quickstart_chains/ethereum-uniswap.md index 3b5ccfeb32c..ff29c604eae 100644 --- a/docs/quickstart/quickstart_chains/ethereum-uniswap.md +++ b/docs/quickstart/quickstart_chains/ethereum-uniswap.md @@ -2,25 +2,41 @@ ## Goals -This project serves as an excellent foundation for initiating your Ethereum SubQuery project. It meticulously indexes various Uniswap entities, including Swaps, Pool operations, Price ticks, Positions, and additionally computes statistical metrics. +Within the domain of DeFi, the objective of indexing Uniswap entities is both crucial and versatile. Essentially, indexing serves as the foundation for transparency and ease of use in the Uniswap system. By systematically organizing tokens, liquidity pools, transactions, and other essential information, indexing provides users with a quick and efficient means to search, find, and analyze data. + +The objective of this article is to offer a detailed, step-by-step guide on setting up a Subquery indexer for Uniswap data indexing. We will comprehensively cover the necessary configurations and delve into the intricacies of the underlying logic. + +Previously, in the [1. Create a New Project](../quickstart.md) section, you must have noted [3 key files](../quickstart.md#_3-make-changes-to-your-project). Let's begin updating them one by one. + +## Setting Up the Indexer + +In this Uniswap indexing project, our main focus is on configuring the indexer to exclusively capture logs generated by three specific types of Uniswap smart contracts: + +1. **UniswapV3Factory** (contract address: `0x1F98431c8aD98523631AE4a59f267346ea31F984`): This contract is responsible for creating all the pool smart contracts. + +2. **Smart Contracts for Individual Pools**: These contracts represent individual liquidity pools. + +3. **NonfungiblePositionManager** (contract address: `0xC36442b4a4522E871399CD717aBDD847Ab11FE88`): This contract is instrumental in producing liquidity positions, which are implemented as NFTs. This functionality enables additional use-cases, including the transfer of liquidity positions. + +To gain a deeper understanding of how these core mechanisms work, you can refer to the official [Uniswap documentation](https://docs.uniswap.org/contracts/v3/reference/deployments). ::: warning -We suggest starting with the [Ethereum Gravatar example](./ethereum-gravatar). The Uniswap project is a lot more complicated and introduces some more advanced concepts. +We suggest starting with the [Ethereum Gravatar example](./ethereum-gravatar). The ENS project is a lot more complicated and introduces some more advanced concepts ::: -Now, let's move forward and fork the example code for this project from [here](https://github.com/subquery/ethereum-subql-starter/tree/main/Ethereum/ethereum-uniswap-v3) +Let's explore the setup for each of these smart contracts to get a complete grasp of their specifics. -## 1. Your Project Manifest File +::: tip Note +The code snippets provided further have been simplified for clarity. You can find the full and detailed code [here](https://github.com/subquery/ethereum-subql-starter/tree/main/Ethereum/ethereum-uniswap-v3) to see all the intricate details. +::: -The Project Manifest (`project.yaml`) file works as an entry point to your Ethereum project. It defines most of the details on how SubQuery will index and transform the chain data. For Ethereum, there are three types of mapping handlers (and you can have more than one in each project): +### UniswapV3Factory -- [BlockHanders](../../build/manifest/ethereum.md#mapping-handlers-and-filters): On each and every block, run a mapping function -- [TransactionHandlers](../../build/manifest/ethereum.md#mapping-handlers-and-filters): On each and every transaction that matches optional filter criteria, run a mapping function -- [LogHanders](../../build/manifest/ethereum.md#mapping-handlers-and-filters): On each and every log that matches optional filter criteria, run a mapping function +The core role of the factory contract is to generate liquidity pool smart contracts. Each pool comprises a pair of two tokens, uniting to create an asset pair, and is associated with a specific fee rate. It's important to emphasize that multiple pools can exist with the same asset pair, distinguished solely by their unique swap fees. -In this Uniswap project, the primary focus is on indexing logs exclusively from two Uniswap smart contracts. LogHandlers, which are the prevalent type of Ethereum handlers, are showcased in this example project, totaling 10 distinct log handlers. +#### 1.Configuring the Manifest File -Additionally, it's important to highlight that this project incorporates six different ABIs. Furthermore, to accommodate scenarios where a contract factory generates new contract instances, [dynamic data sources](../../build/dynamicdatasources.md) are employed. +In simple terms, there's only one event that requires configuration, and that's the `PoolCreated` event. After adding this event to the manifest file, it will be represented as follows: ```yaml dataSources: @@ -41,23 +57,263 @@ dataSources: file: "./abis/ERC20NameBytes.json" Pool: file: "./abis/pool.json" - mapping: ... - # ethereum/contract - - kind: ethereum/Runtime - startBlock: 12369651 - options: - abi: NonfungiblePositionManager - address: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88" - assets: - NonfungiblePositionManager: - file: ./abis/NonfungiblePositionManager.json - Pool: - file: ./abis/pool.json - Factory: - file: ./abis/factory.json - ERC20: - file: ./abis/ERC20.json - mapping: ... + mapping: + file: "./dist/index.js" + handlers: + - handler: handlePoolCreated + kind: ethereum/LogHandler + filter: + topics: + - PoolCreated(address indexed token0, address indexed token1, uint24 indexed fee, int24 tickSpacing, address pool) +``` + +::: tip Note +Check out our [Manifest File](../../build/manifest/ethereum.md) documentation to get more information about the Project Manifest (`project.yaml`) file. +::: + +#### 2. Updating the GraphQL Schema File + +Now, let's consider the entities that we can extract from the factory smart contract for subsequent querying. The most obvious ones include: + +1. `Factory`: This entity represents the factory smart contracts responsible for creating the `Pool` smart contracts. As of the publication of this page, there is currently only one active factory smart contract in use. + +2. `Token`: This entity identifies the token entity, as Pools always involve two tokens. + +3. `Pool`: This entity represents the XYK pools, which serve as the primary trading execution mechanism on UniswapV3. + +For these entities, the following attributes can be derived from data indexed from raw blockchain logs: + +```graphql +type Factory @entity { + # factory address + id: ID! + # amount of pools created + poolCount: BigInt! + # amoutn of transactions all time + txCount: BigInt! + + ... + + # current owner of the factory + owner: ID! +} + +type Token @entity { + # token address + id: ID! + # token symbol + symbol: String! + # token name + name: String! + # token decimals + decimals: BigInt! + # token total supply + totalSupply: BigInt! + + ... + + # pools token is in that are white listed for USD pricing + # Should be Pool + # whitelistPools: [Pool!]! + # derived fields + tokenDayData: [TokenDayData!]! @derivedFrom(field: "token") +} + +type Pool @entity { + # pool address + id: ID! + # creation + createdAtTimestamp: BigInt! + # block pool was created at + createdAtBlockNumber: BigInt! + # token0 + token0: Token + # token0: [Token!] @derivedFrom(field: "id") + token1: Token + # token1: [Token!] @derivedFrom(field: "id") + # current tick + tick: BigInt + # current observation index + observationIndex: BigInt! + # all time token0 swapped + volumeToken0: Float! + + ... + + mints: [Mint!]! @derivedFrom(field: "pool") + burns: [Burn!]! @derivedFrom(field: "pool") + swaps: [Swap!]! @derivedFrom(field: "pool") + collects: [Collect!]! @derivedFrom(field: "pool") + ticks: [Tick!]! @derivedFrom(field: "pool") +} + +``` + +::: tip Note +The attributes mentioned above represent only a subset of the available attributes. For a complete list and detailed documentation, please refer to the final code. +::: + +As you explore these attributes, you may notice the relationship between the `Pool` and `Token` entities. Additionally, you'll find numerous derived attributes like `mints` or `swaps`. + +::: tip Note +Importantly, these relationships not only establish one-to-many connections but also extend to include many-to-many associations. To delve deeper into entity relationships, you can refer to [this section](../../build/graphql.md#entity-relationships). If you prefer a more example-based approach, our dedicated [Hero Course Module](../../academy/herocourse/module3.md) can provide further insights. +::: + +SubQuery simplifies and ensures type-safety when working with GraphQL entities, smart contracts, events, transactions, and logs. The SubQuery CLI will generate types based on your project's GraphQL schema and any contract ABIs included in the data sources. + +::: code-tabs +@tab:active yarn + +```shell +yarn codegen +``` + +@tab npm + +```shell +npm run-script codegen +``` + +::: + +This action will generate a new directory (or update the existing one) named `src/types`. Inside this directory, you will find automatically generated entity classes corresponding to each type defined in your `schema.graphql`. These classes facilitate type-safe operations for loading, reading, and writing entity fields. You can learn more about this process in [the GraphQL Schema section](../../build/graphql.md). + +You can conveniently import all these entities from the following directory: + +```ts +// Import entity types generated from the GraphQL schema +import { Factory, Pool, Token } from "../types"; +``` + +It will also generate a class for every contract event, offering convenient access to event parameters, as well as information about the block and transaction from which the event originated. You can find detailed information on how this is achieved in the [EVM Codegen from ABIs](../../build/introduction.md#evm-codegen-from-abis) section. All of these types are stored in the `src/types/abi-interfaces` and `src/types/contracts` directories. In the example Gravatar SubQuery project, you can import these types as follows: + +#### 3. Writing the Mappings + +Mapping functions define how blockchain data is transformed into the optimized GraphQL entities that we previously defined in the `schema.graphql` file. + +::: tip Note +For more information on mapping functions, please refer to our [Mappings](../../build/mapping/ethereum.md) documentation. +::: + +Writing mappings for the factory smart contract is a straightforward process. To provide better context, we've included this handler in a separate file `factory.ts` within the `src/mappings` directory. Let's start by importing the necessary modules. + +```ts +// Import event types from the registry contract ABI +import { + Pool, + Token, + Factory, + ... +} from "../types"; +import { EthereumLog } from "@subql/types-ethereum"; +import { PoolCreatedEvent } from "../types/contracts/Factory"; +``` + +`Pool`, `Factory`, and `Token` are models that were generated in a [prior step](#2-updating-graphql-schema-file). `PoolCreatedEvent` and `EthereumLog` are TypeScript models generated by the SubQuery SDK to facilitate event handling. + +As a reminder from the configuration step outlined in the [Manifest File](#1configuring-manifest-file), we have a single handler called `handlePoolCreated`. Now, let's proceed with its implementation: + +```ts +export async function handlePoolCreated( + event: EthereumLog +): Promise { + let factory = await Factory.get(FACTORY_ADDRESS); + if (factory === undefined || factory === undefined) { + factory = Factory.create({ + id: FACTORY_ADDRESS, + poolCount: ZERO_BI, + totalVolumeETH: 0, + ... + } + + let [token0, token1] = await Promise.all([ + Token.get(event.args.token0), + Token.get(event.args.token1), + ]); + // fetch info if nul + if (token0 === undefined || token0 == null) { + const [symbol, name, totalSupply, decimals] = await Promise.all([ + fetchTokenSymbol(event.args.token0), + fetchTokenName(event.args.token0), + fetchTokenTotalSupply(event.args.token0).then((r) => r.toBigInt()), + fetchTokenDecimals(event.args.token0), + ]); + // bail if we couldn't figure out the decimals + if (!decimals) { + return; + } + + token0 = Token.create({ + id: event.args.token0, + symbol, + name, + totalSupply, + ... + }); + } + + if (token1 === undefined || token1 == null) { + const [symbol, name, totalSupply, decimals] = await Promise.all([ + fetchTokenSymbol(event.args.token1), + fetchTokenName(event.args.token1), + fetchTokenTotalSupply(event.args.token1).then((r) => r.toBigInt()), + fetchTokenDecimals(event.args.token1), + ]); + // bail if we couldn't figure out the decimals + if (!decimals) { + return; + } + + token1 = Token.create({ + id: event.args.token1, + symbol, + name, + totalSupply, + ... + }); + } + + factory.poolCount = factory.poolCount + ONE_BI; + + const pool = Pool.create({ + id: event.args.pool, + token0Id: token0.id, + token1Id: token1.id, + ... + }); + + await Promise.all([ + token0.save(), + token1.save(), // create the tracked contract based on the template + pool.save(), + factory.save(), + ]); +} +``` + +Explaining the code provided above, the `handlePoolCreated` function accepts an Ethereum event object as its input. This function serves the purpose of capturing essential information when a new pool is created on the blockchain. Here's a breakdown of its key steps: + +1. **Factory Object Retrieval**: Initially, the function tries to retrieve a Factory object. If a Factory object is not found or is undefined, it proceeds to create a new Factory object with default initial values. + +2. **Token Information Retrieval**: Following that, the function fetches information about two tokens: `token0` and `token1`. + +::: tip Note +Throughout this mapping and those that follow, numerous utility functions are employed to process the data. In this specific example, these utility functions are stored in the `utils` directory. If you're interested in understanding how they work, you can refer to the [final code](https://github.com/subquery/ethereum-subql-starter/tree/main/Ethereum/ethereum-uniswap-v3). +::: + +3. **Data Persistence**: To ensure the collected data persists, the function saves the modifications made to the `Token`, `Pool`, and `Factory` objects. This typically entails storing the data in a database or data store. + +🎉 At this point, you have successfully crafted the handling logic for the factory smart contract and populated queryable entities like `Token`, `Pool`, and `Factory`. This means you can now proceed to the [building process](#build-your-project) to test the indexer's functionality up to this point. + +### Pool Smart Contracts + +As we discussed in the introduction of [Configuring the Indexer](#configuring-the-indexer), a new contract is created by the [factory contract](#uniswapv3factory) for each newly created pool. + +#### 1. Configuring the Manifest File + +Additionally, to handle situations where a contract factory generates fresh contract instances for each new pool, we should utilize [dynamic data sources](../../build/dynamicdatasources.md). Consequently, the following adjustments need to be made in the manifest: + +```yaml templates: - name: Pool kind: ethereum/Runtime @@ -70,38 +326,144 @@ templates: file: "./abis/factory.json" ERC20: file: "./abis/ERC20.json" - mapping: ... + mapping: + file: "./dist/index.js" + handlers: + - handler: handleInitialize + kind: ethereum/LogHandler + filter: + topics: + - Initialize (uint160,int24) + - handler: handleSwap + kind: ethereum/LogHandler + filter: + topics: + - Swap (address sender, address recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick) + - handler: handleMint + kind: ethereum/LogHandler + filter: + topics: + - Mint(address sender, address owner, int24 tickLower, int24 tickUpper, uint128 amount, uint256 amount0, uint256 amount1) + - handler: handleBurn + kind: ethereum/LogHandler + filter: + topics: + - Burn(indexed address,indexed int24,indexed int24,uint128,uint256,uint256) + - handler: handleFlash + kind: ethereum/LogHandler + filter: + topics: + - Flash(indexed address,indexed address,uint256,uint256,uint256,uint256) ``` -Check out our [Manifest File](../../build/manifest/ethereum.md) documentation to get more information about the Project Manifest (`project.yaml`) file. - -## 2. Update Your GraphQL Schema File +#### 2. Updating the GraphQL Schema File -The `schema.graphql` file determines the shape of your data from SubQuery due to the mechanism of the GraphQL query language. Hence, updating the GraphQL Schema file is the perfect place to start. It allows you to define your end goal right at the start. - -Within this project, you will observe the presence of 22 GraphQL entities, each interconnected through numerous foreign key relationships. Consider, for instance, the relationship between the `Pool` and `Token` entities. Importantly, these relationships encompass not only one-to-many connections but also extend to encompass many-to-many associations. +Numerous entities can be derived from each newly created pool smart contract. To highlight some of the most crucial ones, you'll need to extend the `schema.graphql` file with the following entities: ```graphql -type Pool @entity { +type Mint @entity { + # transaction hash + "#" + index in mints Transaction array id: ID! + # which txn the mint was included in + transaction: Transaction! + # time of txn + timestamp: BigInt! + # pool position is within + pool: Pool! + # allow indexing by tokens + token0: Token! + # allow indexing by tokens + token1: Token! ... - swaps: [Swap!]! @derivedFrom(field: "pool") + # order within the txn + logIndex: BigInt +} + +type Burn @entity { + # transaction hash + "#" + index in mints Transaction array + id: ID! + # txn burn was included in + transaction: Transaction! + # pool position is within + pool: Pool! + # allow indexing by tokens + token0: Token! + # allow indexing by tokens + token1: Token! ... + # position within the transactions + logIndex: BigInt } type Swap @entity { + # transaction hash + "#" + index in swaps Transaction array id: ID! + # pointer to transaction + transaction: Transaction! + # timestamp of transaction + timestamp: BigInt! + # pool swap occured within + pool: Pool! + # allow indexing by tokens + token0: Token! + # allow indexing by tokens + token1: Token! ... + # index within the txn + logIndex: BigInt +} + +type Collect @entity { + # transaction hash + "#" + index in collect Transaction array + id: ID! + # pointer to txn + transaction: Transaction! + # timestamp of event + timestamp: BigInt! + # pool collect occured within + pool: Pool! + ... + # index within the txn + logIndex: BigInt +} + +type Flash @entity { + # transaction hash + "-" + index in collect Transaction array + id: ID! + # pointer to txn + transaction: Transaction! + # timestamp of event + timestamp: BigInt! + # pool collect occured within pool: Pool! ... + # index within the txn + logIndex: BigInt +} + +type Transaction @entity { + # txn hash + id: ID! + # block txn was included in + blockNumber: BigInt! + # timestamp txn was confirmed + timestamp: BigInt! + # gas used during txn execution + gasUsed: BigInt! + gasPrice: BigInt! + # derived values + mints: [Mint]! @derivedFrom(field: "transaction") + burns: [Burn]! @derivedFrom(field: "transaction") + swaps: [Swap]! @derivedFrom(field: "transaction") + flashed: [Flash]! @derivedFrom(field: "transaction") + collects: [Collect]! @derivedFrom(field: "transaction") } + ``` -::: warning Important -When you make any changes to the schema file, please ensure that you regenerate your types directory. -::: +Similar to the previously imported entities, we observe various relationships here. In this case, each new entity references both the `Token` and `Pool` entities, establishing a one-to-one relationship. Additionally, each new entity references a `Transaction` entity, which is the only one among the newly added entities not derived from logs. Instead, it's derived from an event to a specific transaction, showcasing the capabilities of the Subquery SDK. -SubQuery makes it easy and type-safe to work with your GraphQL entities, as well as smart contracts, events, transactions, and logs. SubQuery CLI will generate types from your project's GraphQL schema and any contract ABIs included in the data sources. +Now, the next step involves instructing the SubQuery CLI to generate types based on your project's updated GraphQL schema: ::: code-tabs @tab:active yarn @@ -118,21 +480,10 @@ npm run-script codegen ::: -This will create a new directory (or update the existing) `src/types` which contain generated entity classes for each type you have defined previously in `schema.graphql`. These classes provide type-safe entity loading, read and write access to entity fields - see more about this process in [the GraphQL Schema](../../build/graphql.md). All entites can be imported from the following directory: - -```ts -// Import entity types generated from the GraphQL schema -import { Bundle, Burn, Factory, Mint, Pool, Swap, Tick, Token } from "../types"; -``` - -As you're creating a new Etheruem based project, this command will also generate ABI types and save them into `src/types` using the `npx typechain --target=ethers-v5` command, allowing you to bind these contracts to specific addresses in the mappings and call read-only contract methods against the block being processed. - -It will also generate a class for every contract event to provide easy access to event parameters, as well as the block and transaction the event originated from. Read about how this is done in [EVM Codegen from ABIs](../../build/introduction.md#evm-codegen-from-abis). - -All of these types are written to `src/typs/abi-interfaces` and `src/typs/contracts` directories. In the example Gravatar SubQuery project, you would import these types like so. +This will create update the existing `src/types` directory. All new entites can now be imported from the following directory: ```ts -// Import event types from the registry contract ABI +import { Burn, Mint, Swap } from "../types"; import { InitializeEvent, MintEvent, @@ -142,17 +493,310 @@ import { } from "../types/contracts/Pool"; ``` -Check out the [GraphQL Schema](../../build/graphql.md) documentation to get in-depth information on `schema.graphql` file. +#### 3. Writing the Mappings + +In this scenario, the mapping process involves two substeps: + +##### Enabling Handling of Newly Created Smart Contracts + +To ensure that all the events mentioned above are handled for any newly created pool smart contract, you'll need to make a minor update to the code in the [factory contract mapping](#3-writing-the-mappings) `factory.ts` file. Once you've executed `subql codegen`, you can include the following import in that file: + +```ts +import { createPoolDatasource } from "../types"; +``` + +Then, within `handlePoolCreated` you will have to add the following: + +```ts +await createPoolDatasource({ + address: event.args.pool, +}); +``` + +After adding the above code to the handler of factory smart contract, you can be sure that the manifested events of all the smart contracts that it produce will be handled. + +##### Writing Pool Smart Contracts Handlers + +The mapping functions for the new entities are a much longer, therefore, in this example, we will only partially incorporate the mapping of `handleSwap` handler: + +```ts +export async function handleSwap( + event: EthereumLog +): Promise { + const poolContract = Pool__factory.connect(event.address, api); + const [ + factory, + pool, + transaction, + ethPrice, + feeGrowthGlobal0X128, + feeGrowthGlobal1X128, + ] = await Promise.all([ + Factory.get(FACTORY_ADDRESS), + Pool.get(event.address), + loadTransaction(event), + getEthPriceInUSD(), + poolContract.feeGrowthGlobal0X128(), + poolContract.feeGrowthGlobal1X128(), + ]); + + const [token0, token1] = await Promise.all([ + Token.get(pool.token0Id), + Token.get(pool.token1Id), + ]); + const oldTick = pool.tick; + + ... + + // global updates + factory.txCount = factory.txCount + ONE_BI; //BigNumber.from(factory.txCount).add(ONE_BI).toBigInt() + factory.totalVolumeETH = + factory.totalVolumeETH + amountTotalETHTracked.toNumber(); //BigNumber.from(factory.totalVolumeETH).add(amountTotalETHTracked).toNumber() + + // updated pool ratess + const prices = sqrtPriceX96ToTokenPrices(pool.sqrtPrice, token0, token1); + + pool.token0Price = prices[0]; + pool.token1Price = prices[1]; + + // create Swap event + // const transaction = await loadTransaction(event) + const swap = Swap.create({ + id: transaction.id + "#" + pool.txCount.toString(), + transactionId: transaction.id, + timestamp: transaction.timestamp, + poolId: pool.id, + token0Id: pool.token0Id, + token1Id: pool.token1Id, + sender: event.args.sender, + origin: event.transaction.from, + recipient: event.args.recipient, + amount0: amount0.toNumber(), + amount1: amount1.toNumber(), + amountUSD: amountTotalUSDTracked.toNumber(), + tick: BigInt(event.args.tick), + sqrtPriceX96: event.args.sqrtPriceX96.toBigInt(), + logIndex: BigInt(event.logIndex), + }); + + await Promise.all([ + swap.save(), + factory.save(), + pool.save(), + token0.save(), + token1.save(), + ]); +} + +``` + +To provide a quick overview of the code above: the function is named `handleSwap` and accepts an Ethereum event object (`event`) as its parameter. It then proceeds to execute several asynchronous operations concurrently using `Promise.all()`. These operations involve fetching data related to the factory, pool, transaction, Ethereum price in USD, and fee growth data from the pool contract. Similarly to the previous step, this code retrieves information about two tokens (`token0` and `token1`) based on their IDs stored in the pool object. + +The code also performs updates to specific global statistics associated with the factory. This includes incrementing the transaction count (`txCount`) and the total volume of ETH traded based on the data acquired during the swap event. + +::: tip Note +For simplicity's sake, we won't delve into a comprehensive explanation of the global statistics metrics. However, in the [final code](https://github.com/subquery/ethereum-subql-starter/tree/main/Ethereum/ethereum-uniswap-v3), you'll find entities with names like `PoolDayData`, `PoolHourData`, `TickHourData`, `TickDayData`, and `TokenHourData`, and their names provide self-explanatory context. +::: + +Furthermore, the code calculates and updates token prices within the pool using the square root price (`sqrtPrice`) of the pool and the tokens involved in the swap. A new `Swap` event object is generated to record the details of the swap transaction. + +Finally, the function saves the updated data for the swap, factory, pool, token0, and token1 objects to ensure that the changes persist in a database or data store. + +🎉 At this stage, you've crafted handling logic for both the factory smart contract and all the pools smart contracts it creates. Additionally, you've populated the project with more entities like `Swap`, `Burn`, and `Mint`, making them queryable. Once again, you can proceed to the [building process](#build-your-project) to test how the indexer operates up to this point. + +### NonfungiblePositionManager + +As you may already know, swaps in UniswapV3 are executed within the context of pools. To enable swaps, these pools must be liquid, and users provide liquidity to each specific pool. Each liquidity provision results in a Liquidity Position, essentially an NFT. This design enables a broader range of DeFi use cases. And the contract responsible for managing these provisions is known as the NonfungiblePositionManager. + +#### 1. Configuring the Manifest File + +For the NonfungiblePositionManager smart contract, we want to introduce the following updates to the manifest file: + +```yaml +- kind: ethereum/Runtime + startBlock: 12369651 + options: + abi: NonfungiblePositionManager + address: "0xC36442b4a4522E871399CD717aBDD847Ab11FE88" + assets: + NonfungiblePositionManager: + file: ./abis/NonfungiblePositionManager.json + Pool: + file: ./abis/pool.json + Factory: + file: ./abis/factory.json + ERC20: + file: ./abis/ERC20.json + mapping: + file: "./dist/index.js" + handlers: + - handler: handleIncreaseLiquidity + kind: ethereum/LogHandler + filter: + topics: + - IncreaseLiquidity (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) + - handler: handleDecreaseLiquidity + kind: ethereum/LogHandler + filter: + topics: + - DecreaseLiquidity (uint256 tokenId, uint128 liquidity, uint256 amount0, uint256 amount1) + - handler: handleCollect + kind: ethereum/LogHandler + filter: + topics: + - Collect (uint256 tokenId, address recipient, uint256 amount0, uint256 amount1) + - handler: handleTransfer + kind: ethereum/LogHandler + filter: + topics: + - Transfer (address from, address to, uint256 tokenId) +``` + +The configuration process closely resembles what we've seen earlier. However, we now have a completely new smart contract that we'll be handling events from. This entails different ABI, address, and start block values. Naturally, it also introduces new events, which are listed under the `handlers` object. + +#### 2. Updating the GraphQL Schema File + +From this smart contract, the only new entity we'll emphasize is the `Position`: + +```graphql +type Position @entity { + # Positions created through NonfungiblePositionManager + # NFT token id + id: ID! + # owner of the NFT + owner: String! + # pool position is within + pool: Pool! + # allow indexing by tokens + token0: Token! + # allow indexing by tokens + token1: Token! + ... + # tx in which the position was initialized + transaction: Transaction! + # vars needed for fee computation + feeGrowthInside0LastX128: BigInt! + feeGrowthInside1LastX128: BigInt! +} +``` + +Once more, we encounter connections to various entities like `Pool` and `Token`. + +Now, the next step involves instructing the SubQuery CLI to generate types based on your project's updated GraphQL schema: + +::: code-tabs +@tab:active yarn + +```shell +yarn codegen +``` + +@tab npm + +```shell +npm run-script codegen +``` + +::: + +This will create update the existing `src/types` directory. All new entites can now be imported from the following directory: + +```ts +import { Position } from "../types"; +import { + IncreaseLiquidityEvent, + DecreaseLiquidityEvent, + CollectEvent, + TransferEvent, +} from "../types/contracts/NonfungiblePositionManager"; +``` + +#### 3. Writing the Mappings + +For this contract, we will craft the mappings in a file named `position-manager.ts`. Once again, this separation provides context and clarity. + +The mapping functions in this file can be extensive. In this example, we will provide a partial implementation of the mapping for the `handleIncreaseLiquidity` handler: + +```ts +export async function handleIncreaseLiquidity( + event: EthereumLog +): Promise { + const position = await getPosition(event, event.args.tokenId); -## 3. Add a Mapping Function + const [token0, token1] = await Promise.all([ + Token.get(position.token0Id), + Token.get(position.token1Id), + ]); -Mapping functions define how chain data is transformed into the optimised GraphQL entities that we previously defined in the `schema.graphql` file. + await updateFeeVars(position, event, event.args.tokenId); -They function in a manner akin to SubGraphs, and you can observe in this Uniswap project that they are distributed across three distinct files, along with the inclusion of various utility files. + await position.save(); -Check out our [Mappings](../../build/mapping/ethereum.md) documentation to get more information on mapping functions. + await savePositionSnapshot(position, event); +} -## 4. Build Your Project +async function getPosition( + event: EthereumLog, + tokenId: BigNumber +): Promise | null { + let position = await Position.get(tokenId.toString()); + + if (position === undefined) { + const contract = NonfungiblePositionManager__factory.connect( + event.address, + api + ); + let positionResult; + try { + positionResult = await contract.positions(tokenId); + } catch (e) { + logger.warn( + `Contract ${event.address}, could not get position with tokenId ${tokenId}` + ); + return null; + } + + const [poolAddress, transaction] = await Promise.all([ + factoryContract.getPool( + positionResult[2], + positionResult[3], + positionResult[4] + ), + loadTransaction(event), + ]); + position = Position.create({ + id: tokenId.toString(), + owner: ADDRESS_ZERO, + poolId: poolAddress, + token0Id: positionResult[2], + token1Id: positionResult[3], + tickLowerId: `${poolAddress}#${positionResult[5].toString()}`, + tickUpperId: `${poolAddress}#${positionResult[6].toString()}`, + liquidity: ZERO_BI, + depositedToken0: 0, //ZERO_BD.toNumber(), + depositedToken1: 0, //ZERO_BD.toNumber(), + withdrawnToken0: 0, //ZERO_BD.toNumber(), + withdrawnToken1: 0, //ZERO_BD.toNumber(), + collectedFeesToken0: 0, //ZERO_BD.toNumber(), + collectedFeesToken1: 0, //ZERO_BD.toNumber(), + transactionId: transaction.id, + feeGrowthInside0LastX128: positionResult[8].toBigInt(), + feeGrowthInside1LastX128: positionResult[9].toBigInt(), + }); + } + return position; +} +``` + +To briefly clarify the code provided above: the handler function `handleIncreaseLiquidity` is responsible for managing an increase in liquidity event on the blockchain. Initially, it invokes the `getPosition` function to either retrieve an existing liquidity position associated with the event's `tokenId` or create a new one if necessary. Any modifications made to the `Position` object during this process are saved to ensure persistence. + +🎉 In conclusion, we have successfully incorporated all the desired entities that can be retrieved from various smart contracts. For each of these entities, we've created mapping handlers to structure and store the data in a queryable format. + +::: tip Note +Check the final code repository [here](https://github.com/subquery/ethereum-subql-starter/tree/main/Ethereum/ethereum-uniswap-v3) to observe the integration of all previously mentioned configurations into a unified codebase. +::: + +## Build Your Project Next, build your work to run your new SubQuery project. Run the build command from the project's root directory as given here: @@ -177,7 +821,7 @@ Whenever you make changes to your mapping functions, you must rebuild your proje Now, you are ready to run your first SubQuery project. Let’s check out the process of running your project in detail. -## 5. Run Your Project Locally with Docker +## Run Your Project Locally with Docker Whenever you create a new SubQuery Project, first, you must run it locally on your computer and test it and using Docker is the easiest and quickiest way to do this. @@ -206,7 +850,7 @@ npm run-script start:docker It may take a few minutes to download the required images and start the various nodes and Postgres databases. ::: -## 6. Query your Project +## Query your Project Next, let's query our project. Follow these three simple steps to query your SubQuery project: @@ -216,44 +860,247 @@ Next, let's query our project. Follow these three simple steps to query your Sub 3. Find the _Docs_ tab on the right side of the playground which should open a documentation drawer. This documentation is automatically generated and it helps you find what entities and methods you can query. -Try the following query to understand how it works for your new SubQuery starter project. Don’t forget to learn more about the [GraphQL Query language](../../run_publish/query.md). +Try the following queries to understand how it works for your new SubQuery starter project. Don’t forget to learn more about the [GraphQL Query language](../../run_publish/query.md). + +### Pools + +#### Request ```graphql query { - positions { + pools(first: 5) { nodes { - id - owner - poolId - liquidity - pool { - id - token0Id - token1Id - } + token0Id + token1Id + token0Price + token1Price + txCount + volumeToken0 + volumeToken1 + } + } +} +``` + +#### Response + +```json +{ + "data": { + "pools": { + "nodes": [ + { + "token0Id": "0x4a220E6096B25EADb88358cb44068A3248254675", + "token1Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "token0Price": 0, + "token1Price": 0, + "txCount": "88", + "volumeToken0": 7035, + "volumeToken1": 61 + }, + { + "token0Id": "0x7Ef7AdaE450e33B4187fe224cAb1C45d37f7c411", + "token1Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "token0Price": 0, + "token1Price": 0, + "txCount": "1", + "volumeToken0": 0, + "volumeToken1": 0 + }, + { + "token0Id": "0x1337DEF16F9B486fAEd0293eb623Dc8395dFE46a", + "token1Id": "0x6B175474E89094C44Da98b954EedeAC495271d0F", + "token0Price": 1, + "token1Price": 1, + "txCount": "34", + "volumeToken0": 15588, + "volumeToken1": 17059 + } + ] } } - swaps { +} +``` + +### Swaps + +#### Request + +```graphql +{ + swaps(first: 3) { nodes { - id token0Id token1Id amount0 amount1 + id + amountUSD + timestamp + transactionId } } } ``` - +#### Response + +```json +{ + "data": { + "swaps": { + "nodes": [ + { + "token0Id": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "token1Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "amount0": -19973, + "amount1": 5, + "id": "0xa670c80538614ce0a2bd3f8071b3c9b51ae4a7a72d9c6405212118895ebe741b#2144", + "amountUSD": 0, + "timestamp": "1620402695", + "transactionId": "0xa670c80538614ce0a2bd3f8071b3c9b51ae4a7a72d9c6405212118895ebe741b" + }, + { + "token0Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "token1Id": "0xf65B5C5104c4faFD4b709d9D60a185eAE063276c", + "amount0": 0, + "amount1": 2000, + "id": "0x44be483f706bb88213a065acdbd9bcbafc3eee68fd95f7bcb7e88808e777bf87#376", + "amountUSD": 0, + "timestamp": "1620269842", + "transactionId": "0x44be483f706bb88213a065acdbd9bcbafc3eee68fd95f7bcb7e88808e777bf87" + }, + { + "token0Id": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "token1Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "amount0": -1043, + "amount1": 0, + "id": "0x36f0f7de5a8ba5a2b6556917d329fb9631b70efd4ada284d504b52cf74fc4d98#2916", + "amountUSD": 0, + "timestamp": "1620433130", + "transactionId": "0x36f0f7de5a8ba5a2b6556917d329fb9631b70efd4ada284d504b52cf74fc4d98" + } + ] + } + } +} +``` -::: tip Note -The final code of this project can be found [here](https://github.com/subquery/ethereum-subql-starter/tree/main/Ethereum/ethereum-uniswap-v3). -::: +### Positions + +#### Request + +```graphql +{ + positions(first: 3) { + nodes { + id + liquidity + tickLowerId + tickUpperId + token0Id + token1Id + transactionId + } + } +} +``` + +#### Response + +```json +{ + "data": { + "positions": { + "nodes": [ + { + "id": "5096", + "liquidity": "1406248200435775", + "tickLowerId": "0xe6868579CA50EF3F0d02d003E6D3e45240efCB35#-129150", + "tickUpperId": "0xe6868579CA50EF3F0d02d003E6D3e45240efCB35#-129140", + "token0Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "token1Id": "0xD46bA6D942050d489DBd938a2C909A5d5039A161", + "transactionId": "0xb097dc1b4eeafd7e770ca5f8de66ffb8117a64dee3e00dd66ae2ee8acf1deb30" + }, + { + "id": "1567", + "liquidity": "0", + "tickLowerId": "0x0000000000000000000000000000000000000000#193380", + "tickUpperId": "0x0000000000000000000000000000000000000000#196260", + "token0Id": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", + "token1Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "transactionId": "0x112f8d32c821a10d91d333738b7b5f5db8ce19ed7c7d7bea5fd1d5f20f39816e" + }, + { + "id": "6480", + "liquidity": "105107889867915697479", + "tickLowerId": "0x0000000000000000000000000000000000000000#-42600", + "tickUpperId": "0x0000000000000000000000000000000000000000#-40200", + "token0Id": "0xB6Ca7399B4F9CA56FC27cBfF44F4d2e4Eef1fc81", + "token1Id": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + "transactionId": "0x714cb4b06338185106289ad291cc787cacabda030747d7870f9830b91e503347" + } + ] + } + } +} +``` + +### uniswapDayData (statistical data) + +#### Request + +```graphql +{ + uniswapDayData(last: 3) { + nodes { + volumeUSD + volumeETH + txCount + date + feesUSD + } + } +} +``` + +#### Response + +```json +{ + "data": { + "uniswapDayData": { + "nodes": [ + { + "volumeUSD": 0, + "volumeETH": 0, + "txCount": "174", + "date": 1620237271, + "feesUSD": 0 + }, + { + "volumeUSD": 0, + "volumeETH": 0, + "txCount": "12741", + "date": 1620291964, + "feesUSD": 0 + }, + { + "volumeUSD": 0, + "volumeETH": 0, + "txCount": "10396", + "date": 1620282149, + "feesUSD": 0 + } + ] + } + } +} +``` ## What's next? -Congratulations! You have now a locally running SubQuery project that accepts GraphQL API requests for transferring data. +Congratulations! You have now a locally running SubQuery project that indexes the major Uniswap entities and accepts GraphQL API requests for transferring data. ::: tip Tip