diff --git a/docs/.vuepress/config.ts b/docs/.vuepress/config.ts index d3e462e6a0e..6767356f915 100644 --- a/docs/.vuepress/config.ts +++ b/docs/.vuepress/config.ts @@ -434,6 +434,10 @@ function getSidebar(locale: string): SidebarOptions { text: "Ethereum Gravatar (Simple)", link: `${locale}/quickstart/quickstart_chains/ethereum-gravatar.md`, }, + { + text: "Ethereum Uniswap (Complex)", + link: `${locale}/quickstart/quickstart_chains/ethereum-uniswap.md`, + }, { text: "Ethereum ENS (Complex)", link: `${locale}/quickstart/quickstart_chains/ethereum-ens.md`, diff --git a/docs/quickstart/quickstart_chains/ethereum-uniswap.md b/docs/quickstart/quickstart_chains/ethereum-uniswap.md new file mode 100644 index 00000000000..e7fadbdba6c --- /dev/null +++ b/docs/quickstart/quickstart_chains/ethereum-uniswap.md @@ -0,0 +1,1121 @@ +# Ethereum Quick Start - Uniswap (Complex) + +## Goals + +Uniswap is one of the leading decentralised exchange (DEX) in web3 and is one that relies on indexers to serve data to it's UI so users can interact with it. By systematically organizing tokens, liquidity pools, transactions, and other essential information, Indexers like SubQuery provide users with a quick and efficient means to search, find, and analyze data within Uniswap. + +The objective of this article is to offer a detailed, step-by-step guide on setting up a SubQuery indexer for Uniswap v3 protocol. We will comprehensively cover the necessary configurations and delve into the intricacies of the underlying logic. It's an excellent example of how to do indexing for a complex DEX like Uniswap. + +## 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 Important +We suggest starting with the [Ethereum Gravatar example](./ethereum-gravatar). The Ethereum Uniswap project is a lot more complicated and introduces some more advanced concepts +::: + +In the earlier section titled "Create a New Project" (refer to [quickstart.md](../quickstart.md)), you should have taken note of three crucial files. To initiate the setup of a project from scratch, you can proceed to follow the steps outlined in the [initialization description](../quickstart.md#2-initialise-a-new-subquery-project). As a prerequisite, you will need to generate types from the ABI files of each smart contract. You can obtain these ABI files by searching for the ABIs of the mentioned smart contract addresses on Etherscan. For instance, you can locate the ABI for **UniswapV3Factory** at the bottom of [this page](https://etherscan.io/address/0x1F98431c8aD98523631AE4a59f267346ea31F984#code). Additionally, you can kickstart your project by using the EVM Scaffolding approach (detailed [here](../quickstart.md#evm-project-scaffolding)). You'll find all the relevant events to be scaffolded in the documentation for each type of smart contract, as described in sections [1](#1-configuring-the-manifest-file), [2](#1-configuring-the-manifest-file-1), and [3](#1configuring-the-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. +::: + +### UniswapV3Factory + +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. + +#### 1.Configuring the Manifest File + +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: + - kind: ethereum/Runtime + startBlock: 12369621 + options: + # Must be a key of assets + abi: Factory + address: "0x1F98431c8aD98523631AE4a59f267346ea31F984" + assets: + Factory: + file: "./abis/Factory.json" + ERC20: + file: "./abis/ERC20.json" + ERC20SymbolBytes: + file: "./abis/ERC20SymbolBytes.json" + ERC20NameBytes: + file: "./abis/ERC20NameBytes.json" + Pool: + file: "./abis/pool.json" + 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. + +#### 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 + +The contract factory generates fresh contract instances for each new pool, therefore we use [dynamic data sources](../../build/dynamicdatasources.md) to create indexers for each new contract: + +```yaml +templates: + - name: Pool + kind: ethereum/Runtime + options: + abi: Pool + assets: + Pool: + file: "./abis/pool.json" + Factory: + file: "./abis/factory.json" + ERC20: + file: "./abis/ERC20.json" + 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) +``` + +#### 2. Updating the GraphQL Schema File + +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 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! + ... + # 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") +} + +``` + +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. + +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 { Burn, Mint, Swap } from "../types"; +import { + InitializeEvent, + MintEvent, + BurnEvent, + SwapEvent, + FlashEvent, +} from "../types/contracts/Pool"; +``` + +#### 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); + + const [token0, token1] = await Promise.all([ + Token.get(position.token0Id), + Token.get(position.token1Id), + ]); + + await updateFeeVars(position, event, event.args.tokenId); + + await position.save(); + + await savePositionSnapshot(position, event); +} + +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: + +::: code-tabs +@tab:active yarn + +```shell +yarn build +``` + +@tab npm + +```shell +npm run-script build +``` + +::: + +::: warning Important +Whenever you make changes to your mapping functions, you must rebuild your project. +::: + +Now, you are ready to run your first SubQuery project. Letโ€™s check out the process of running your project in detail. + +## 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. + +The `docker-compose.yml` file defines all the configurations that control how a SubQuery node runs. For a new project, which you have just initialised, you won't need to change anything. + +However, visit the [Running SubQuery Locally](../../run_publish/run.md) to get more information on the file and the settings. + +Run the following command under the project directory: + +::: code-tabs +@tab:active yarn + +```shell +yarn start:docker +``` + +@tab npm + +```shell +npm run-script start:docker +``` + +::: + +::: tip Note +It may take a few minutes to download the required images and start the various nodes and Postgres databases. +::: + +## Query your Project + +Next, let's query our project. Follow these three simple steps to query your SubQuery project: + +1. Open your browser and head to `http://localhost:3000`. + +2. You will see a GraphQL playground in the browser and the schemas which are ready to query. + +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 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 { + pools(first: 5) { + nodes { + 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 + +#### Request + +```graphql +{ + swaps(first: 3) { + nodes { + 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" + } + ] + } + } +} +``` + +
+ +
+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 indexes the major Uniswap entities and accepts GraphQL API requests for transferring data. + +::: tip Tip + +Find out how to build a performant SubQuery project and avoid common mistakes in [Project Optimisation](../../build/optimisation.md). + +::: + +Click [here](../../quickstart/whats-next.md) to learn what should be your **next step** in your SubQuery journey.